Back to DevOpsForge Visual Builder
Fullstack Architecture

Next.js Docker Compose with PostgreSQL & Caddy (Auto-SSL)

Updated September 2026 Standalone Multi-Stage Build Next.js 14 / 15 Ready

Self-hosting Next.js in Docker has become the gold standard for indie hackers and engineering teams looking to break free from costly managed cloud tiers. However, containerizing Next.js is notoriously prone to pitfalls: bloated 1.5GB image sizes, broken static file routes (/_next/static 404s), and missing automated HTTPS reverse proxies.

This battle-tested production architecture pairs Next.js in standalone output mode (producing lean ~120MB Alpine containers) with a hardened PostgreSQL 16 database, Redis cache, and Caddy v2 for automatic Let's Encrypt SSL.

Customize or Run this Stack Visually

Tweak ports, toggle Redis caching, or generate automated backup scripts with zero manual YAML editing.

1. Production `docker-compose.yml`

This configuration spins up Next.js on port 3000, persistent PostgreSQL, Redis, and Caddy with automatic HTTPS termination and health checks.

docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    environment:
      - NODE_ENV=production
      - PORT=3000
      - HOSTNAME=0.0.0.0
      - DATABASE_URL=postgresql://db_user:db_password@postgres:5432/app_db
      - REDIS_URL=redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/"]
      interval: 30s
      timeout: 10s
      retries: 3
    networks:
      - app_network

  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: db_user
      POSTGRES_PASSWORD: db_password
      POSTGRES_DB: app_db
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U db_user -d app_db"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app_network

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    networks:
      - app_network

  proxy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - app
    networks:
      - app_network

networks:
  app_network:
    driver: bridge

volumes:
  pgdata:
  redisdata:
  caddy_data:
  caddy_config:

2. Production Multi-Stage `Dockerfile` (Standalone Mode)

Leverages Next.js output file tracing to copy only the files strictly needed by the server. Drops container size from ~1.5GB to under ~120MB, running under an unprivileged non-root user.

Dockerfile
# --- Stage 1: Install Dependencies ---
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
  else npm install; \
  fi

# --- Stage 2: Build Application ---
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

# --- Stage 3: Minimal Production Runner ---
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs

# Copy static assets and traced standalone bundle
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "server.js"]

3. Configuring `next.config.js` for Standalone Output

Ensure your next.config.js enables standalone output tracing. This creates the self-contained .next/standalone folder required by the Dockerfile:

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  reactStrictMode: true,
};

module.exports = nextConfig;

4. Production `Caddyfile` with Automatic SSL

Replaces complex Nginx proxy configs with clean, automatic TLS certificate management and HTTP/3 support.

Caddyfile
yourdomain.com {
    encode gzip zstd

    # Route all requests to the Next.js standalone container
    reverse_proxy app:3000

    # Security Headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}