{"slug":"how-to-deploy-docker-compose-to-a-vps","url":"https://tako.sh/blog/how-to-deploy-docker-compose-to-a-vps/","canonical":"https://tako.sh/blog/how-to-deploy-docker-compose-to-a-vps/","title":"How to Deploy Docker Compose to a VPS with Automatic HTTPS","date":"2026-11-02T03:23","description":"Deploy a Bun and Redis Docker Compose stack to one VPS with Tako: local HTTPS, private networking, named volumes, and automatic production TLS.","author":null,"image":"0d360d0553c5","imageAlt":"A coral octopus opens a secure web-service dock for visitors while Redis stays connected behind the gate.","headings":[{"depth":2,"slug":"run-the-bun-and-redis-stack-locally","text":"Run the Bun and Redis stack locally"},{"depth":2,"slug":"prepare-the-vps-and-select-the-http-service","text":"Prepare the VPS and select the HTTP service"},{"depth":2,"slug":"deploy-and-verify-the-counter-and-its-data","text":"Deploy and verify the counter and its data"}],"markdown":"> **Networking update.** The server setup described here uses Tako’s legacy VPN integration. For new installations, see [native WireGuard networks](/blog/native-wireguard-networks-for-vps-apps/). For an existing Tailscale or NetBird host, follow the [migration guide](/blog/migrate-tailscale-to-tako-wireguard/).\n\nYour app already has a Compose file: one web service, a Redis dependency, and a named volume. You want to deploy that stack to a VPS, give it a real HTTPS hostname, and keep its data when you ship the next version.\n\nTako now supports that shape directly. We'll deploy the shipped Bun-and-Redis example, test its counter through local HTTPS, then verify the same behavior on a public domain. The app uses its existing command and needs no Tako SDK.\n\n## Run the Bun and Redis stack locally\n\nStart with the [complete example](https://github.com/tako-sh/tako/tree/master/examples/compose/bun-redis). Copy it into its own directory so your application files are easy to inspect:\n\n```bash\ngit clone --depth 1 https://github.com/tako-sh/tako.git tako-examples\ncp -R tako-examples/examples/compose/bun-redis ./bun-redis\ncd bun-redis\n```\n\nKeep the copied `.dockerignore`, Dockerfile, `server.ts`, Compose file, and `tako.toml`. The Bun server handles two routes: `/` increments a Redis counter and returns JSON containing `visits`; `/health` checks Redis without incrementing anything. There is no dependency install or SDK integration to add.\n\nIts `compose.yaml` is:\n\n```yaml\nservices:\n  web:\n    build: .\n    restart: unless-stopped\n    ports:\n      - \"8080:3000\"\n    environment:\n      REDIS_URL: redis://redis:6379\n    depends_on:\n      redis:\n        condition: service_healthy\n    healthcheck:\n      test:\n        - CMD\n        - bun\n        - -e\n        - \"process.exit((await fetch('http://127.0.0.1:3000/health')).ok ? 0 : 1)\"\n      interval: 5s\n      timeout: 3s\n      retries: 10\n  redis:\n    image: docker.io/library/redis:7-alpine\n    restart: unless-stopped\n    command: [redis-server, --appendonly, \"yes\"]\n    volumes:\n      - redis-data:/data\n    healthcheck:\n      test: [CMD, redis-cli, ping]\n      interval: 5s\n      timeout: 3s\n      retries: 10\n\nvolumes:\n  redis-data:\n```\n\nThe app connects to Redis through its service name. `depends_on` waits for Redis's health check, and the web health check exercises the app's Redis connection. Docker's [service reference](https://docs.docker.com/reference/compose-file/services/) describes these dependency and health-check settings; Tako's [Compose guide](/docs/docker-compose/) defines the supported production subset.\n\nThe Dockerfile builds the web image from the included server:\n\n```dockerfile\nFROM oven/bun:1-alpine\nWORKDIR /app\nCOPY --chown=bun:bun server.ts ./server.ts\nUSER bun\nEXPOSE 3000\nCMD [\"bun\", \"server.ts\"]\n```\n\nInstall the Tako CLI if needed:\n\n```bash\ncurl -fsSL https://tako.sh/install.sh | sh\n```\n\nWith Docker Engine and Compose 2.24.4 or newer available locally, run:\n\n```bash\ndocker compose version\ndocker info\ntako dev\n```\n\nTako builds and starts the whole stack, waits for health, and serves the web service through its local HTTPS hostname. Open the URL it reports and request `/` twice: the `visits` values should increase. Request `/health` to check the dependency without changing the counter.\n\nPress `r` in the dev session, or run `tako dev --restart`, after changing the image or Compose configuration. Stopping and restarting the app preserves named volumes. Each checkout and dev variant has its own Compose project, so a different checkout starts with separate data. See [Local development](/docs/development/) for the surrounding HTTPS setup.\n\nFor a standalone Compose check, `docker compose up --build --wait` uses the original `8080:3000` mapping. Tako replaces that mapping with private ingress in its own development project. Pick one workflow for your test and use its corresponding URL.\n\n## Prepare the VPS and select the HTTP service\n\nUse a supported Linux VPS with glibc, systemd, and Tako's required isolation controllers. Connect your workstation and server through Tailscale or NetBird, allowing SSH and private management TCP `9844`. Follow [Servers](/docs/servers/) for host requirements and enrollment.\n\nFor example, on an existing NetBird network:\n\n```bash\ntako servers add ubuntu@my-server.netbird.cloud --vpn netbird --name prod\n```\n\nReplace the administrator and hostname with your actual values. Interactive setup offers to install or repair Tako when needed. A Tailscale setup uses its own server hostname and `--vpn tailscale` instead.\n\n**Install Docker Engine and the Compose plugin separately on the VPS.** Compose deployments require Docker and Compose **2.24.4 or newer**. The Tako installer currently installs Podman for standalone image releases; that does not satisfy the Compose requirement. Use [Docker's distribution-specific installation instructions](https://docs.docker.com/engine/install/).\n\nThe `tako-server` service account must be able to access the local Docker socket. For a conventional rootful Docker installation with the `docker` group, an administrator can configure a fresh host like this:\n\n```bash\nsudo usermod -aG docker tako\nsudo systemctl restart tako-server\nsudo -u tako docker compose version\nsudo -u tako docker info\n```\n\nThe restart lets the service acquire its new group membership. Docker documents that this group grants root-level privileges in its [post-installation guide](https://docs.docker.com/engine/install/linux-postinstall/). Check access as `tako`, rather than relying on a successful `docker info` from your administrator account.\n\nBack in the app directory, edit the example's `tako.toml`:\n\n```toml\nname = \"bun-redis\"\n\n[compose]\nfile = \"compose.yaml\"\nservice = \"web\"\nport = 3000\nhealth_path = \"/health\"\n\n[envs.production]\nroute = \"app.example.com\"\nservers = [\"prod\"]\n```\n\n`service` selects the HTTP ingress service; `port` is that service's container port. It is `3000`, not the standalone host port `8080`. The health path must return HTTP 2xx or 3xx. If your own app restricts Host headers, configure `health_host` to a hostname it accepts.\n\nPoint your public domain at the VPS's public address and allow public HTTP `80` and HTTPS `443`. For this direct-to-VPS example, Tako's default Let's Encrypt mode uses HTTP-01 validation. Declaring a route does not create DNS records; [Routing and TLS](/docs/routing/) covers the certificate choices.\n\nPublic traffic and private service traffic take different paths:\n\n```d2\ndirection: right\nbrowser: \"Browser\"\nvps: \"One VPS\" {\n  proxy: \"Tako\\npublic HTTPS\" {style.fill: \"#E88783\"}\n  web: \"Bun web\\ncontainer port 3000\" {style.fill: \"#9BC4B6\"}\n  redis: \"Redis\\nCompose network\"\n  volume: \"redis-data\\nnamed volume\"\n  proxy -> web: \"assigned loopback port\"\n  web -> redis: \"redis:6379\"\n  redis -> volume\n}\nbrowser -> vps.proxy: \"app.example.com\"\n```\n\nTako replaces Compose port publications and exposes only the selected HTTP service through assigned loopback ports. Redis stays on the Compose network; visitors reach the app through Tako's HTTPS route.\n\n## Deploy and verify the counter and its data\n\nDeploy from the copied application directory:\n\n```bash\ntako deploy --env production\ntako status --json\ntako logs --env production --days 1 --json\n```\n\nTako captures and validates the production inputs, prepares the images, and starts the services. Once the selected web service passes readiness checks, Tako admits it to public traffic. The [deployment guide](/docs/deployment/) covers release preparation and failure handling.\n\nRequest the real domain, substituting yours below:\n\n```bash\ncurl --fail --silent --show-error https://app.example.com/health\ncurl --fail --silent --show-error https://app.example.com/\ncurl --fail --silent --show-error https://app.example.com/\n```\n\nConfirm a trusted certificate, a successful health response, and increasing `visits` values. Record the observed counter, make a small change to the greeting in `server.ts`, and deploy again. Request `/` afterward: it should contain the new greeting and continue beyond the earlier counter, rather than starting from empty data. We're checking both changed application code and preserved Redis state.\n\nThe stable app/environment project identity reuses the named volume across releases. Redis's append-only setting stores its data there. Docker's [volume reference](https://docs.docker.com/reference/compose-file/volumes/) explains named-volume declarations; Tako additionally scopes resources to the app and environment and rejects external resources, custom resource names, and anonymous volumes in production.\n\n| Check | What it establishes |\n| --- | --- |\n| `docker info` as `tako` | Service-account access to the server's Docker Engine |\n| Successful `/health` | The web process can reach Redis |\n| Increasing `visits` | Public requests exercise the application and database |\n| Counter continues after a code update | Named-volume data survives the release change |\n\nThis is a single-VPS deployment with one selected HTTP ingress service. Keep build inputs and file references within the application directory; writable bind mounts are unsupported in production. If your stack uses secrets, reference Tako secrets explicitly for Compose interpolation. Production does not load an implicit `.env` file. The [Compose reference](/docs/docker-compose/) documents these boundaries.\n\nNamed volumes survive stop/start, rollback, and application deletion, but Tako's app-data backups do not include them. Back up Redis separately and verify restoration into a separate stack. Keeping a volume is persistence; recovering a lost VPS requires a backup.\n\nOnce the first deploy works, continue with [Compose updates and persistent volumes](/blog/docker-compose-updates-with-persistent-volumes/) or [scaling individual Compose services](/blog/scale-docker-compose-services-on-a-vps/). Start with the counter test: one real write before and after a release tells you more than a green deploy message alone."}