VPS
Learn how to deploy your TurboStarter app to a VPS.
For the complete documentation index, see llms.txt. Prefer markdown by appending.mdto documentation URLs or sendingAccept: text/markdown.
A VPS gives you full control over the runtime, network, reverse proxy, backups, and deployment cadence. It is a good option if you want predictable costs, long-running Node.js processes, or a single server that hosts your app, proxy, and supporting services.
This guide explains how to deploy the TurboStarter web app to a VPS using Docker, Docker Compose, and Caddy as a reverse proxy with automatic HTTPS.
Prerequisites
Before deploying, make sure you have:
- a VPS running Ubuntu or another Linux distribution
- a domain pointed to the VPS public IP address
- Docker and Docker Compose installed on the server
- a production database with migrations already applied
- your production environment variables ready
- a Dockerfile configured for the web app, as described in the Docker guide
Prepare the server
Connect to your server over SSH and install the basic runtime packages:
sudo apt update
sudo apt install -y git ca-certificates curlInstall Docker using the official Docker installation guide. After Docker is installed, verify that Compose is available:
docker compose versionKeep ports 80 and 443 open
Your reverse proxy needs inbound HTTP and HTTPS traffic. If your VPS provider has a firewall, allow ports 80 and 443. Keep the app container private and expose only the reverse proxy to the internet.
Clone the repository
Clone your project on the server:
git clone <your-repository-url> turbostarter
cd turbostarterIf you deploy from a private repository, use a deploy key or a fine-scoped access token. Avoid using a personal token with access to all repositories on the server.
Configure production environment variables
Create production environment files on the server. The root file should contain shared values like the database URL and app URL:
NODE_ENV="production"
DATABASE_URL="postgresql://..."
URL="https://example.com"
BETTER_AUTH_URL="https://example.com"
NEXT_PUBLIC_URL="https://example.com"
BETTER_AUTH_SECRET="..."Add app-specific variables in apps/web/.env.production:
NEXT_PUBLIC_PRODUCT_NAME="${PRODUCT_NAME}"
NEXT_PUBLIC_SITE_LINK="${URL}"
NEXT_PUBLIC_SITE_TITLE="TurboStarter"
NEXT_PUBLIC_SITE_DESCRIPTION="Production-ready SaaS starter kit"
RESEND_API_KEY="..."
EMAIL_FROM="..."Use the environment variables guide as the source of truth for your project. The exact list depends on the features you enabled, such as billing, analytics, emails, storage, AI providers, or background jobs.
Public variables are build-time variables
Variables prefixed with NEXT_PUBLIC_ are bundled into the client app during the Docker build. If you change the production URL or another public variable, rebuild the image and restart the container.
Run database migrations
Run migrations before sending traffic to the app. The recommended approach is to use the GitHub Actions workflow shipped with TurboStarter and set DATABASE_URL as a repository secret.
You can also run migrations locally against the production database:
pnpm with-env pnpm --filter @workspace/db db:migratePrefer a managed production database
For most apps, use a managed Postgres provider such as Supabase, Neon, Railway, Render, or DigitalOcean Managed Databases. Hosting Postgres on the same VPS is possible, but you must own backups, upgrades, storage monitoring, and recovery.
Create Docker Compose configuration
Create a production Docker Compose file in the repository root:
services:
web:
build:
context: .
dockerfile: apps/web/Dockerfile
restart: unless-stopped
env_file:
- ./.env.production
- ./apps/web/.env.production
environment:
NODE_ENV: production
PORT: 3000
expose:
- "3000"
caddy:
image: caddy:2-alpine
restart: unless-stopped
depends_on:
- web
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:Then create the Caddy configuration:
example.com {
encode zstd gzip
reverse_proxy web:3000
header {
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
}
}Replace example.com with your production domain. Caddy will automatically request and renew TLS certificates when the domain points to the VPS and ports 80 and 443 are reachable.
Deploy the app
Build and start the production stack:
docker compose -f compose.production.yml up -d --buildWatch the logs after the first deployment:
docker compose -f compose.production.yml logs -f web
docker compose -f compose.production.yml logs -f caddyOpen your domain and verify the main production flows:
- sign in and sign out
- database reads and writes
- organization creation
- billing checkout and webhooks
- email sending
- file uploads, if your app uses storage
First deployment may need one redeploy
If you did not know the final domain before the first build, update URL, NEXT_PUBLIC_URL, BETTER_AUTH_URL, OAuth callbacks, and billing webhook URLs, then rebuild the image.
Update the deployment
For future releases, pull the latest changes and rebuild the stack:
git pull
docker compose -f compose.production.yml up -d --buildIf you want to free disk space after several deployments, prune unused images:
docker image prune -fFor zero-downtime or multi-instance deployments, put a load balancer in front of multiple app containers or move to a platform with rolling deploys. For a single VPS, expect a short restart window during updates.
That's it! Your TurboStarter web app is now running on your own VPS with Docker, a reverse proxy, and HTTPS.
Production checklist
Before launch, also review the deployment checklist, configure OAuth callbacks for your final domain, point billing webhooks to /api/webhooks/billing, and make sure your database backups are enabled.
How is this guide?
Last updated on