Docker Deployment

Containerize your Next.js application for deployment anywhere.

Dockerfile#

1# Dockerfile 2FROM node:20-alpine AS base 3 4# Install dependencies only when needed 5FROM base AS deps 6RUN apk add --no-cache libc6-compat 7WORKDIR /app 8 9# Install dependencies based on the preferred package manager 10COPY package.json package-lock.json* ./ 11COPY prisma ./prisma/ 12RUN npm ci 13 14# Rebuild the source code only when needed 15FROM base AS builder 16WORKDIR /app 17COPY --from=deps /app/node_modules ./node_modules 18COPY . . 19 20# Generate Prisma Client 21RUN npx prisma generate 22 23# Build the application 24ENV NEXT_TELEMETRY_DISABLED 1 25RUN npm run build 26 27# Production image, copy all the files and run next 28FROM base AS runner 29WORKDIR /app 30 31ENV NODE_ENV production 32ENV NEXT_TELEMETRY_DISABLED 1 33 34RUN addgroup --system --gid 1001 nodejs 35RUN adduser --system --uid 1001 nextjs 36 37COPY --from=builder /app/public ./public 38 39# Set the correct permission for prerender cache 40RUN mkdir .next 41RUN chown nextjs:nodejs .next 42 43# Automatically leverage output traces to reduce image size 44COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 45COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 46 47# Copy prisma schema for migrations 48COPY --from=builder /app/prisma ./prisma 49COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma 50 51USER nextjs 52 53EXPOSE 3000 54 55ENV PORT 3000 56ENV HOSTNAME "0.0.0.0" 57 58CMD ["node", "server.js"]

Next.js Configuration#

1// next.config.js 2/** @type {import('next').NextConfig} */ 3const nextConfig = { 4 output: 'standalone', 5 experimental: { 6 // Optimize for Docker 7 outputFileTracingRoot: process.cwd(), 8 }, 9}; 10 11module.exports = nextConfig;

Docker Compose#

1# docker-compose.yml 2version: '3.8' 3 4services: 5 app: 6 build: 7 context: . 8 dockerfile: Dockerfile 9 ports: 10 - "3000:3000" 11 environment: 12 - DATABASE_URL=postgresql://postgres:postgres@db:5432/app 13 - NEXT_PUBLIC_APP_URL=http://localhost:3000 14 depends_on: 15 db: 16 condition: service_healthy 17 restart: unless-stopped 18 19 db: 20 image: postgres:15-alpine 21 environment: 22 - POSTGRES_USER=postgres 23 - POSTGRES_PASSWORD=postgres 24 - POSTGRES_DB=app 25 volumes: 26 - postgres_data:/var/lib/postgresql/data 27 healthcheck: 28 test: ["CMD-SHELL", "pg_isready -U postgres"] 29 interval: 10s 30 timeout: 5s 31 retries: 5 32 33 redis: 34 image: redis:7-alpine 35 volumes: 36 - redis_data:/data 37 healthcheck: 38 test: ["CMD", "redis-cli", "ping"] 39 interval: 10s 40 timeout: 5s 41 retries: 5 42 43volumes: 44 postgres_data: 45 redis_data:

.dockerignore#

# .dockerignore .git .gitignore .next node_modules npm-debug.log README.md .env*.local .vercel .turbo coverage .nyc_output *.md !README.md Dockerfile* docker-compose*

Build and Run#

1# Build image 2docker build -t my-nextjs-app . 3 4# Run container 5docker run -p 3000:3000 \ 6 -e DATABASE_URL="postgresql://..." \ 7 -e NEXT_PUBLIC_APP_URL="http://localhost:3000" \ 8 my-nextjs-app 9 10# With docker-compose 11docker-compose up -d 12 13# View logs 14docker-compose logs -f app 15 16# Stop 17docker-compose down

Database Migrations#

In Dockerfile Entry Point#

1# Create entrypoint script 2COPY docker-entrypoint.sh /usr/local/bin/ 3RUN chmod +x /usr/local/bin/docker-entrypoint.sh 4 5ENTRYPOINT ["docker-entrypoint.sh"] 6CMD ["node", "server.js"]
1#!/bin/sh 2# docker-entrypoint.sh 3 4set -e 5 6# Run migrations 7echo "Running database migrations..." 8npx prisma migrate deploy 9 10# Start the application 11exec "$@"

Via Docker Compose#

1# docker-compose.yml 2services: 3 migrate: 4 build: . 5 command: npx prisma migrate deploy 6 environment: 7 - DATABASE_URL=postgresql://postgres:postgres@db:5432/app 8 depends_on: 9 db: 10 condition: service_healthy 11 12 app: 13 build: . 14 depends_on: 15 migrate: 16 condition: service_completed_successfully

Multi-Stage for Development#

1# Dockerfile.dev 2FROM node:20-alpine 3 4WORKDIR /app 5 6# Install dependencies 7COPY package.json package-lock.json ./ 8RUN npm install 9 10# Copy source 11COPY . . 12 13# Generate Prisma Client 14RUN npx prisma generate 15 16EXPOSE 3000 17 18CMD ["npm", "run", "dev"]
1# docker-compose.dev.yml 2version: '3.8' 3 4services: 5 app: 6 build: 7 context: . 8 dockerfile: Dockerfile.dev 9 ports: 10 - "3000:3000" 11 volumes: 12 - .:/app 13 - /app/node_modules 14 - /app/.next 15 environment: 16 - DATABASE_URL=postgresql://postgres:postgres@db:5432/app 17 depends_on: 18 - db 19 20 db: 21 image: postgres:15-alpine 22 environment: 23 - POSTGRES_USER=postgres 24 - POSTGRES_PASSWORD=postgres 25 - POSTGRES_DB=app 26 ports: 27 - "5432:5432" 28 volumes: 29 - postgres_data:/var/lib/postgresql/data 30 31volumes: 32 postgres_data:

GitHub Container Registry#

1# .github/workflows/docker.yml 2name: Build and Push Docker Image 3 4on: 5 push: 6 branches: [main] 7 tags: ['v*'] 8 9env: 10 REGISTRY: ghcr.io 11 IMAGE_NAME: ${{ github.repository }} 12 13jobs: 14 build: 15 runs-on: ubuntu-latest 16 permissions: 17 contents: read 18 packages: write 19 20 steps: 21 - uses: actions/checkout@v4 22 23 - name: Set up Docker Buildx 24 uses: docker/setup-buildx-action@v3 25 26 - name: Log in to Container Registry 27 uses: docker/login-action@v3 28 with: 29 registry: ${{ env.REGISTRY }} 30 username: ${{ github.actor }} 31 password: ${{ secrets.GITHUB_TOKEN }} 32 33 - name: Extract metadata 34 id: meta 35 uses: docker/metadata-action@v5 36 with: 37 images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 38 tags: | 39 type=ref,event=branch 40 type=semver,pattern={{version}} 41 type=sha,prefix= 42 43 - name: Build and push 44 uses: docker/build-push-action@v5 45 with: 46 context: . 47 push: true 48 tags: ${{ steps.meta.outputs.tags }} 49 labels: ${{ steps.meta.outputs.labels }} 50 cache-from: type=gha 51 cache-to: type=gha,mode=max

Kubernetes Deployment#

1# k8s/deployment.yaml 2apiVersion: apps/v1 3kind: Deployment 4metadata: 5 name: nextjs-app 6spec: 7 replicas: 3 8 selector: 9 matchLabels: 10 app: nextjs-app 11 template: 12 metadata: 13 labels: 14 app: nextjs-app 15 spec: 16 containers: 17 - name: nextjs-app 18 image: ghcr.io/your-org/your-app:latest 19 ports: 20 - containerPort: 3000 21 env: 22 - name: DATABASE_URL 23 valueFrom: 24 secretKeyRef: 25 name: app-secrets 26 key: database-url 27 resources: 28 requests: 29 memory: "256Mi" 30 cpu: "100m" 31 limits: 32 memory: "512Mi" 33 cpu: "500m" 34 readinessProbe: 35 httpGet: 36 path: /api/health 37 port: 3000 38 initialDelaySeconds: 10 39 periodSeconds: 5 40 livenessProbe: 41 httpGet: 42 path: /api/health 43 port: 3000 44 initialDelaySeconds: 30 45 periodSeconds: 10 46--- 47apiVersion: v1 48kind: Service 49metadata: 50 name: nextjs-app 51spec: 52 selector: 53 app: nextjs-app 54 ports: 55 - port: 80 56 targetPort: 3000 57 type: ClusterIP 58--- 59apiVersion: networking.k8s.io/v1 60kind: Ingress 61metadata: 62 name: nextjs-app 63 annotations: 64 kubernetes.io/ingress.class: nginx 65 cert-manager.io/cluster-issuer: letsencrypt-prod 66spec: 67 tls: 68 - hosts: 69 - your-app.com 70 secretName: nextjs-app-tls 71 rules: 72 - host: your-app.com 73 http: 74 paths: 75 - path: / 76 pathType: Prefix 77 backend: 78 service: 79 name: nextjs-app 80 port: 81 number: 80

Health Check#

1// app/api/health/route.ts 2import { NextResponse } from 'next/server'; 3 4export async function GET() { 5 return NextResponse.json({ 6 status: 'healthy', 7 timestamp: new Date().toISOString(), 8 version: process.env.npm_package_version || 'unknown', 9 }); 10}