BACK TO BLOG
DockerNginxDevOpsNext.js
From Zero to Production: Dockerizing a Full-Stack App with Nginx
Step-by-step guide to containerizing a Next.js + Express app, setting up Nginx reverse proxy, and achieving zero-downtime deployments.
December 10, 20257 min read
The Goal
Take a Next.js frontend + Express backend running locally and deploy it to a VPS with:
- Both services in Docker containers
- Nginx as a reverse proxy (single port 443 exposed)
- Zero-downtime deployments via rolling restarts
- SSL via Let's Encrypt
Project Structure
app/
├── frontend/ # Next.js
├── backend/ # Express
├── nginx/
│ └── nginx.conf
└── docker-compose.yml
Dockerfiles
Frontend:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.next/standalone .
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
Backend:
FROM node:20-alpine
WORKDIR /app
COPY package*.json .
RUN npm ci --only=production
COPY . .
EXPOSE 4000
CMD ["node", "index.js"]
Nginx Config
upstream frontend { server frontend:3000; }
upstream backend { server backend:4000; }
server {
listen 443 ssl;
server_name yourdomain.com;
location / {
proxy_pass http://frontend;
proxy_set_header Host $host;
}
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
}
}
docker-compose.yml
services:
frontend:
build: ./frontend
restart: unless-stopped
backend:
build: ./backend
restart: unless-stopped
env_file: .env
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- frontend
- backend
Zero-Downtime Deployments
The trick is to rebuild and restart one service at a time:
# Pull latest code
git pull origin main
# Rebuild and restart backend (frontend stays up)
docker compose up -d --build --no-deps backend
# Then frontend
docker compose up -d --build --no-deps frontend
Nginx keeps routing to the old container until the new one is healthy. No downtime.
Health Checks
Add this to each service in docker-compose.yml so Nginx only routes to healthy containers:
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
interval: 10s
timeout: 5s
retries: 3
This is exactly the setup I use at Saciva — it's been running for months without a single unplanned outage.
All PostsRana Dolui