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_healthycondition in Docker Compose prevents this. - Prisma Client Engine Missing in Alpine: Add
RUN apk add --no-cache opensslto your Dockerfile base stage if Prisma reports missing OpenSSL binary targets.