Dockerizing a Full-Stack Next.js 15 and PostgreSQL App for Production (Step-by-Step 2026)

Production-ready guide to dockerizing Next.js 15 with standalone build mode, PostgreSQL 17, Prisma migrations, and health checks with minimal image footprint.

Deploying full-stack Next.js applications with self-hosted PostgreSQL databases often turns into a nightmare when developers push bloated multi-gigabyte Docker images containing development tools, test suites, and unoptimized node_modules.

In 2026, leveraging Next.js 15 Standalone Output Mode alongside Multi-Stage Alpine Dockerfiles slashes final container size from 1.8 GB down to under 120 MB, dramatically accelerating CI/CD deployment pipelines and reducing memory footprints.

Core Best Practices in Production

Never run Node.js as the root user inside your container. Always leverage a dedicated unprivileged user (nextjs:nodejs), use healthchecks to guard against database connection timeouts, and run database migrations prior to server launch.

1. Multi-Stage Dockerfile (Node 22-Alpine)

Save the following Dockerfile at your project root:

# Stage 1: Base Alpine Environment
FROM node:22-alpine AS base
WORKDIR /app
RUN apk add --no-cache libc6-compat
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

# Stage 2: Dependencies
FROM base AS deps
COPY package.json pnpm-lock.yaml* package-lock.json* ./
RUN npm ci

# Stage 3: Builder
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN npm run build

# Stage 4: Production Runner (< 120MB)
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

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

# Copy standalone output & static assets
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
CMD ["node", "server.js"]

2. Production Docker Compose (App + PostgreSQL 17)

version: '3.8'

services:
  postgres:
    image: postgres:17-alpine
    container_name: production-postgres
    restart: always
    environment:
      POSTGRES_USER: app_admin
      POSTGRES_PASSWORD: StrongProductionPassword2026!
      POSTGRES_DB: production_db
    volumes:
      - pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app_admin -d production_db"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  nextjs:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: production-nextjs-app
    restart: always
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://app_admin:StrongProductionPassword2026!@postgres:5432/production_db?schema=public
      NEXTAUTH_SECRET: supersecretjwtkey2026
      NEXTAUTH_URL: https://yourdomain.com
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - app-network

volumes:
  pg_data:
    driver: local

networks:
  app-network:
    driver: bridge

3. Crucial Configuration in next.config.ts

Ensure output: 'standalone' is explicitly enabled in your Next.js configuration:

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

module.exports = nextConfig;

4. Common Troubleshooting Scenarios

  • Error: ECONNREFUSED postgres:5432: Next.js attempted to query the database before PostgreSQL finished initialization. The service_healthy condition in Docker Compose prevents this.
  • Prisma Client Engine Missing in Alpine: Add RUN apk add --no-cache openssl to your Dockerfile base stage if Prisma reports missing OpenSSL binary targets.