Skip to content

Docker Compose

This guide covers a complete production deployment of TheTerms using Docker Compose. The stack runs three services: the Next.js app, PostgreSQL 16, and Redis 7.

  • Docker Engine 20+ and Docker Compose v2+
  • A server with at least 1 CPU core, 2 GB RAM, and 10 GB disk
  • A domain name with DNS pointed at your server (for HTTPS)
  1. Clone the repository

    Terminal window
    git clone https://github.com/ashwineaso/theterms.git
    cd theterms
  2. Create your environment file

    Terminal window
    cp apps/web/.env.example .env
  3. Configure required variables

    Edit .env and set at minimum:

    Terminal window
    NEXTAUTH_SECRET= # openssl rand -base64 32
    NEXTAUTH_URL= # https://your-domain.com
    POSTGRES_PASSWORD= # a strong unique password

    See Environment Variables for the full reference.

  4. Build and start all services

    Terminal window
    docker compose up -d --build
  5. Verify

    Terminal window
    docker compose ps

    All three services should show Up (healthy). Open http://your-server:3000.

The docker-compose.yml defines three services:

ServiceImagePortPurpose
appBuilt from Dockerfile${PORT:-3000}:3000Next.js application
postgrespostgres:16-alpineinternalPostgreSQL database
redisredis:7-alpineinternalRedis cache

Only the app service is exposed externally. Postgres and Redis are internal to the Docker network.

services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "${PORT:-3000}:3000"
environment:
DATABASE_URL: "postgresql://postgres:${POSTGRES_PASSWORD:-password}@postgres:5432/theterms"
REDIS_URL: "redis://redis:6379"
NEXTAUTH_URL: "${NEXTAUTH_URL}"
NEXTAUTH_SECRET: "${NEXTAUTH_SECRET}"
NEXT_PUBLIC_APP_URL: "${NEXTAUTH_URL}"
RESEND_API_KEY: "${RESEND_API_KEY:-}"
EMAIL_FROM: "${EMAIL_FROM:-TheTerms <noreply@example.com>}"
GOOGLE_CLIENT_ID: "${GOOGLE_CLIENT_ID:-}"
GOOGLE_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET:-}"
MICROSOFT_CLIENT_ID: "${MICROSOFT_CLIENT_ID:-}"
MICROSOFT_CLIENT_SECRET: "${MICROSOFT_CLIENT_SECRET:-}"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: theterms
POSTGRES_USER: postgres
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-password}"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
volumes:
postgres_data:
redis_data:

The Dockerfile uses a four-stage build to minimise the final image size:

  1. base — Alpine + Node.js with corepack enabled for pnpm
  2. deps — Installs all dependencies (including dev) with pnpm
  3. builder — Builds the Next.js app with pnpm build, generates Prisma client
  4. runner — Minimal Alpine image with only the standalone Next.js output, Prisma CLI, and schema file

The runner stage uses output: "standalone" from Next.js, which copies only the necessary files and bundles a minimal node_modules. The Prisma CLI is installed separately in runner so it can run migrations on startup.

Every time the app container starts, docker-entrypoint.sh runs before the application:

Terminal window
prisma migrate deploy --schema=./packages/db/prisma/schema.prisma

This applies any pending migrations against the database. The app only starts serving traffic after migrations complete successfully. If migrations fail, the container exits with an error.

Both postgres and redis have healthchecks configured. The app service uses depends_on: condition: service_healthy, so it won’t start until both dependencies are ready. This prevents connection errors on first boot.

Terminal window
docker compose logs -f app # App logs (streaming)
docker compose logs postgres # Database logs
docker compose logs redis # Redis logs