VPS

Learn how to deploy your TurboStarter app to a VPS.

For the complete documentation index, see llms.txt. Prefer markdown by appending .md to documentation URLs or sending Accept: 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 curl

Install Docker using the official Docker installation guide. After Docker is installed, verify that Compose is available:

docker compose version

Keep 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 turbostarter

If 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:

.env.production
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:

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:migrate

Prefer 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:

compose.production.yml
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:

Caddyfile
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 --build

Watch the logs after the first deployment:

docker compose -f compose.production.yml logs -f web
docker compose -f compose.production.yml logs -f caddy

Open 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 --build

If you want to free disk space after several deployments, prune unused images:

docker image prune -f

For 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

On this page

Ship your startup everywhere. In minutes.Try TurboStarter