How to Host Multiple Apps on One VPS with Automatic HTTPS
One VPS is enough for more apps than people give it credit for.
The 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.
Tako’s model is simpler: every app owns its own 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.
This walkthrough hosts three apps on one VPS:
| App | Route | Job |
|---|---|---|
www | example.com, www.example.com | Marketing site |
api | api.example.com, example.com/api/* | HTTP API |
docs | example.com/docs/* | Docs app under a path prefix |
One server. Three deployments. Automatic HTTPS for the public hostnames.
What you need
You need a Linux VPS, a domain, the local tako 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.
On your laptop:
curl -fsSL https://tako.sh/install.sh | sh
On the VPS:
sudo sh -c "$(curl -fsSL https://tako.sh/install-server.sh)"
Then register the server once:
tako servers add prod.example-tailnet.ts.net --name prod
For public app traffic, point DNS at the VPS public IP:
| DNS record | Value |
|---|---|
example.com | A / AAAA record to the VPS |
www.example.com | A / AAAA record to the VPS |
api.example.com | A / AAAA record to the VPS |
You 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.
Give each app a stable identity
Each 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.
That separation matters on a shared box:
| Piece | Separated by app identity? |
|---|---|
| Release directories | Yes |
| Runtime processes | Yes |
| Routes | Yes |
| Secrets | Yes |
TAKO_DATA_DIR app data | Yes |
| Scale setting | Yes |
The marketing app can be a Next.js app:
# apps/www/tako.toml
name = "www"
runtime = "node"
preset = "nextjs"
[envs.production]
routes = ["example.com", "www.example.com"]
servers = ["prod"]
The API can be a Bun app:
# apps/api/tako.toml
name = "api"
runtime = "bun"
[envs.production]
routes = ["api.example.com", "example.com/api/*"]
servers = ["prod"]
The docs app can live under a path prefix:
# apps/docs/tako.toml
name = "docs"
runtime = "bun"
preset = "vite"
assets = ["dist/client"]
[build]
run = "bun run build"
[envs.production]
route = "example.com/docs/*"
servers = ["prod"]
Deploy them independently:
cd apps/www && tako deploy
cd ../api && tako deploy
cd ../docs && tako deploy
Each 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.
How route matching works
At 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.
Specific 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.
| Request | Selected app | Why |
|---|---|---|
https://example.com/ | www | Host-only route matches |
https://www.example.com/ | www | Exact hostname route matches |
https://api.example.com/users | api | Exact API hostname route matches |
https://example.com/api/users | api | Longer /api/* path route beats host-only route |
https://example.com/docs/start | docs | Longer /docs/* path route beats host-only route |
Tako 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.
The 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.
Static assets under path prefixes
Static 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/*.
Tako 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.
| Request | Matched route | Static lookup candidates |
|---|---|---|
/docs/assets/main.js | example.com/docs/* | /docs/assets/main.js, then /assets/main.js |
/docs/logo.png | example.com/docs/* | /docs/logo.png, then /logo.png |
Your 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.
This 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.
Automatic HTTPS per route
When 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.
At 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.
For the three-app setup:
| Route | Certificate behavior |
|---|---|
example.com | Public certificate via HTTP-01 |
www.example.com | Public certificate via HTTP-01 |
api.example.com | Public certificate via HTTP-01 |
example.com/docs/* | Uses the example.com certificate |
Wildcard 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:
tako credentials set ssl.cloudflare --env production
Then 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.
The box stays understandable
After the three deploys, tako servers status gives you the server view:
✓ prod up
┌ www (production) running
│ instances: 1/1
└ deployed: ...
┌ api (production) running
│ instances: 1/1
└ deployed: ...
┌ docs (production) running
│ instances: 1/1
└ deployed: ...
You can scale each app separately:
cd apps/api
tako scale 2 --env production
cd ../docs
tako scale 0 --env production
The 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.
That 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.
Start with one app, then add the second one without changing the first. The deployment docs cover the full deploy flow, and how Tako works explains the proxy/process architecture underneath.