{"slug":"how-to-host-multiple-apps-on-one-vps-with-automatic-https","url":"https://tako.sh/blog/how-to-host-multiple-apps-on-one-vps-with-automatic-https/","canonical":"https://tako.sh/blog/how-to-host-multiple-apps-on-one-vps-with-automatic-https/","title":"How to Host Multiple Apps on One VPS with Automatic HTTPS","date":"2026-05-08T02:24","description":"Run several apps on one VPS with Tako routes, SNI certificate selection, static assets, and automatic HTTPS for every domain.","author":null,"image":"97424f86ce07","imageAlt":null,"headings":[{"depth":2,"slug":"what-you-need","text":"What you need"},{"depth":2,"slug":"give-each-app-a-stable-identity","text":"Give each app a stable identity"},{"depth":2,"slug":"how-route-matching-works","text":"How route matching works"},{"depth":2,"slug":"static-assets-under-path-prefixes","text":"Static assets under path prefixes"},{"depth":2,"slug":"automatic-https-per-route","text":"Automatic HTTPS per route"},{"depth":2,"slug":"the-box-stays-understandable","text":"The box stays understandable"}],"markdown":"One VPS is enough for more apps than people give it credit for.\n\nThe hard part is not CPU. The hard part is the pile of glue around the apps: one Nginx config per hostname, one process manager stanza per service, one Certbot renewal path, one static-file exception, one \"why is `/api` hitting the wrong app?\" debugging session. Do that three times and your small server starts feeling like a tiny operations department.\n\nTako's model is simpler: every app owns its own [`tako.toml`](/docs/tako-toml/), every environment declares the routes it wants, and `tako-server` builds one route table across the box. Pingora terminates HTTPS on `:443`, selects the certificate by SNI, matches the request host/path, serves static assets when it can, and forwards the rest to the right app process.\n\nThis walkthrough hosts three apps on one VPS:\n\n| App    | Route                                  | Job                          |\n| ------ | -------------------------------------- | ---------------------------- |\n| `www`  | `example.com`, `www.example.com`       | Marketing site               |\n| `api`  | `api.example.com`, `example.com/api/*` | HTTP API                     |\n| `docs` | `example.com/docs/*`                   | Docs app under a path prefix |\n\nOne server. Three deployments. Automatic HTTPS for the public hostnames.\n\n## What you need\n\nYou need a Linux VPS, a domain, the local [`tako` CLI](/docs/cli/), and `tako-server` installed on the box. The server installer sets up the Rust server binary, the service manager unit, the `tako` control user, the `tako-app` runtime user, port binding capabilities, and the Pingora proxy.\n\nOn your laptop:\n\n```bash\ncurl -fsSL https://tako.sh/install.sh | sh\n```\n\nOn the VPS:\n\n```bash\nsudo sh -c \"$(curl -fsSL https://tako.sh/install-server.sh)\"\n```\n\nThen register the server once:\n\n```bash\ntako servers add prod.example-tailnet.ts.net --name prod\n```\n\nFor public app traffic, point DNS at the VPS public IP:\n\n| DNS record        | Value                          |\n| ----------------- | ------------------------------ |\n| `example.com`     | `A` / `AAAA` record to the VPS |\n| `www.example.com` | `A` / `AAAA` record to the VPS |\n| `api.example.com` | `A` / `AAAA` record to the VPS |\n\nYou do not need one server per app. You do not need one reverse-proxy config per hostname. The app route declarations become the proxy config.\n\n## Give each app a stable identity\n\nEach app gets its own project directory and its own `tako.toml`. Set `name` explicitly. Tako can infer a name from the directory, but top-level `name` is the stable server-side identity. A production deploy of `www` lives under the app identity `www/production`; a production deploy of `api` lives under `api/production`.\n\nThat separation matters on a shared box:\n\n| Piece                    | Separated by app identity? |\n| ------------------------ | -------------------------- |\n| Release directories      | Yes                        |\n| Runtime processes        | Yes                        |\n| Routes                   | Yes                        |\n| Secrets                  | Yes                        |\n| `TAKO_DATA_DIR` app data | Yes                        |\n| Scale setting            | Yes                        |\n\nThe marketing app can be a Next.js app:\n\n```toml\n# apps/www/tako.toml\nname = \"www\"\nruntime = \"node\"\npreset = \"nextjs\"\n\n[envs.production]\nroutes = [\"example.com\", \"www.example.com\"]\nservers = [\"prod\"]\n```\n\nThe API can be a Bun app:\n\n```toml\n# apps/api/tako.toml\nname = \"api\"\nruntime = \"bun\"\n\n[envs.production]\nroutes = [\"api.example.com\", \"example.com/api/*\"]\nservers = [\"prod\"]\n```\n\nThe docs app can live under a path prefix:\n\n```toml\n# apps/docs/tako.toml\nname = \"docs\"\nruntime = \"bun\"\npreset = \"vite\"\nassets = [\"dist/client\"]\n\n[build]\nrun = \"bun run build\"\n\n[envs.production]\nroute = \"example.com/docs/*\"\nservers = [\"prod\"]\n```\n\nDeploy them independently:\n\n```bash\ncd apps/www && tako deploy\ncd ../api && tako deploy\ncd ../docs && tako deploy\n```\n\nEach deploy updates one app. The others keep serving. If you ship a docs typo, the API does not restart. If you rotate API secrets, the marketing app does not care.\n\n## How route matching works\n\nAt runtime, Tako has one route table per server. Every deployed app contributes its routes. Incoming requests are matched by host and path, then sent to the selected app's load balancer.\n\n```d2\ndirection: right\n\nbrowser: Browser\n\nproxy: \"Pingora proxy\\n:443\" {\n  style.fill: \"#FFF9F4\"\n  style.stroke: \"#2F2A44\"\n}\n\ntable: \"Tako route table\" {\n  style.fill: \"#9BC4B6\"\n}\n\nwww: \"www app\\nexample.com\"\napi: \"api app\\napi.example.com\\nexample.com/api/*\"\ndocs: \"docs app\\nexample.com/docs/*\"\n\nbrowser -> proxy: \"HTTPS request\"\nproxy -> table: \"host + path\"\ntable -> www: \"example.com/\"\ntable -> api: \"example.com/api/users\"\ntable -> docs: \"example.com/docs/start\"\n```\n\nSpecific routes win. Exact host beats wildcard host. Longer path beats shorter path. A host-only route like `example.com` can serve normal page traffic, while `example.com/api/*` and `example.com/docs/*` carve out subtrees for other apps.\n\n| Request                          | Selected app | Why                                               |\n| -------------------------------- | ------------ | ------------------------------------------------- |\n| `https://example.com/`           | `www`        | Host-only route matches                           |\n| `https://www.example.com/`       | `www`        | Exact hostname route matches                      |\n| `https://api.example.com/users`  | `api`        | Exact API hostname route matches                  |\n| `https://example.com/api/users`  | `api`        | Longer `/api/*` path route beats host-only route  |\n| `https://example.com/docs/start` | `docs`       | Longer `/docs/*` path route beats host-only route |\n\nTako validates this at deploy time. Routes must include a hostname. A non-development environment must define `route` or `routes`. A single environment can use `route` for one route or `routes` for many, but not both. Deploy conflict detection prevents overlapping routes from silently stealing traffic.\n\nThe practical rule: use exact hostnames when you can, path prefixes when you want one apex domain to feel like several apps, and wildcard routes only when the app really owns tenant subdomains.\n\n## Static assets under path prefixes\n\nStatic assets are where path-prefix hosting usually gets annoying. A docs bundle might emit `/assets/main.js`, but visitors request it as `/docs/assets/main.js` because the app is mounted under `example.com/docs/*`.\n\nTako handles that in the proxy. For static asset requests, `tako-server` looks in the deployed app's `public/` directory. When the matched route has a path prefix, it also tries the prefix-stripped path.\n\n| Request                | Matched route        | Static lookup candidates                       |\n| ---------------------- | -------------------- | ---------------------------------------------- |\n| `/docs/assets/main.js` | `example.com/docs/*` | `/docs/assets/main.js`, then `/assets/main.js` |\n| `/docs/logo.png`       | `example.com/docs/*` | `/docs/logo.png`, then `/logo.png`             |\n\nYour app keeps its normal build output, while Tako makes subpath deployment work at the edge. If no static file exists, the request falls through to the app process.\n\nThis is also why the `assets` field matters. Presets can provide default asset roots, and top-level `assets` can add more. During deploy, those assets are merged into the app's deployed `public/` directory, where the proxy can serve them directly before waking or forwarding to the app.\n\n## Automatic HTTPS per route\n\nWhen you deploy a public route, Tako asks for the certificate it needs. For normal public hostnames, it uses ACME with Let's Encrypt and the HTTP-01 challenge on port 80. Let's Encrypt's challenge docs describe HTTP-01 as a token served from `/.well-known/acme-challenge/<TOKEN>` on port 80; Tako's proxy handles that challenge path before app routing.\n\nAt TLS handshake time, the browser sends SNI for the hostname. Tako looks up the matching certificate, tries wildcard fallback when appropriate, and serves a fallback self-signed certificate only when no matching certificate exists yet so the connection can still complete and return a normal HTTP status.\n\nFor the three-app setup:\n\n| Route                | Certificate behavior               |\n| -------------------- | ---------------------------------- |\n| `example.com`        | Public certificate via HTTP-01     |\n| `www.example.com`    | Public certificate via HTTP-01     |\n| `api.example.com`    | Public certificate via HTTP-01     |\n| `example.com/docs/*` | Uses the `example.com` certificate |\n\nWildcard routes are the special case. If you deploy `*.example.com`, HTTP-01 cannot prove control of every possible subdomain. Tako supports wildcard certificates through Cloudflare DNS-01. Set up the provider credential first:\n\n```bash\ntako credentials set ssl.cloudflare --env production\n```\n\nThen deploy the wildcard route. If the app environment is missing Cloudflare credentials and declares a Let’s Encrypt wildcard route, deploy fails with a setup hint instead of leaving you with a route that cannot get the right certificate.\n\n## The box stays understandable\n\nAfter the three deploys, `tako servers status` gives you the server view:\n\n```text\n✓ prod up\n  ┌ www (production) running\n  │ instances: 1/1\n  └ deployed: ...\n  ┌ api (production) running\n  │ instances: 1/1\n  └ deployed: ...\n  ┌ docs (production) running\n  │ instances: 1/1\n  └ deployed: ...\n```\n\nYou can scale each app separately:\n\n```bash\ncd apps/api\ntako scale 2 --env production\n\ncd ../docs\ntako scale 0 --env production\n```\n\nThe API can stay warm with two instances. The docs app can scale to zero and wake on request. Both decisions persist across deploys and server restarts because desired instance count is server runtime state, not a line in `tako.toml`.\n\nThat is the point: the server stays one server, but the apps stay separate apps. Routes decide traffic. SNI decides certificates. App identity decides disk paths, secrets, logs, data, releases, and scaling.\n\nStart with one app, then add the second one without changing the first. The [deployment docs](/docs/deployment/) cover the full deploy flow, and [how Tako works](/docs/how-tako-works/) explains the proxy/process architecture underneath."}