Docker and CI/CD Pipeline for Next.js Applications
A complete guide to containerising Next.js with Docker multi-stage builds and setting up a fully automated CI/CD pipeline with GitHub Actions for zero-downtime deployments.
Containerisation and CI/CD are the two pillars of modern DevOps. Combining Docker with GitHub Actions fully automates the journey from code commit to production deployment, eliminating human error and compressing release cycles to minutes.
1. Optimised Dockerfile for Next.js
Multi-stage builds reduce image size from ~1 GB down to ~150 MB.
# Stage 1: Dependencies
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Stage 2: Builder
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: Runner (production image)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
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"]
Enable standalone output in next.config.ts:
const nextConfig = { output: 'standalone' };
export default nextConfig;
2. Docker Compose for Local Development
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile
target: runner
ports:
- "3000:3000"
environment:
- DATABASE_URL=${DATABASE_URL}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 10s
timeout: 5s
retries: 5
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- app
volumes:
postgres_data:
3. GitHub Actions CI/CD Pipeline
# .github/workflows/deploy.yml
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run typecheck
- run: npm run lint
- run: npm run build
env:
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to VPS via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /opt/app
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f
echo "Deploy completed at $(date)"
4. Secrets Management
Never hardcode secrets into Dockerfiles or docker-compose files. Use GitHub Secrets for CI/CD and .env files locally (already gitignored).
gh secret set DATABASE_URL --body "postgresql://..."
gh secret set VPS_SSH_KEY < ~/.ssh/id_rsa
gh secret set NEXTAUTH_SECRET --body "$(openssl rand -base64 32)"
5. Health Check Endpoint and Auto-rollback
// app/api/health/route.ts
export async function GET() {
return Response.json({
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version,
});
}
#!/bin/bash
# deploy.sh — deploy with automatic rollback on health check failure
set -e
PREVIOUS_IMAGE=$(docker inspect app --format='{{.Image}}' 2>/dev/null || echo "")
docker compose pull
docker compose up -d --remove-orphans
sleep 30
if ! curl -sf http://localhost:3000/api/health; then
echo "Health check failed, rolling back..."
docker compose down
docker tag $PREVIOUS_IMAGE ghcr.io/org/app:latest
docker compose up -d
exit 1
fi
echo "Deploy successful"
Conclusion
A CI/CD pipeline with Docker and GitHub Actions gives your team the confidence to deploy multiple times per day. With the configuration above, every pull request automatically runs tests, builds a Docker image, and deploys to production in 3–5 minutes. Ventra Rocket applies this pipeline to all client production projects.
Related Articles
Kubernetes Deployment Guide for Node.js Applications
Step-by-step guide to deploying Node.js on Kubernetes — Deployments, Services, HPA, health checks, and zero-downtime rollouts.
Microservices Architecture with Docker and Message Queues
Design patterns for building microservices — service decomposition, async communication with RabbitMQ, circuit breakers, distributed tracing, and observability.
CI/CD with GitHub Actions: Build, Test, and Deploy Pipelines
Automating software delivery — build pipelines, testing, Docker image builds, deployment automation, and secrets management with GitHub Actions.