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 /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 /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 /app/.next/standalone ./
45COPY /app/.next/static ./.next/static
46
47# Copy prisma schema for migrations
48COPY /app/prisma ./prisma
49COPY /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 downDatabase 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_successfullyMulti-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=maxKubernetes 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: 80Health 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}