Introduction
Zero‑downtime deployments are a non‑negotiable expectation for modern services. As a DevOps lead, you want a repeatable, low‑risk process that lets you push new code without interrupting users. This checklist walks you through a Docker‑centric workflow backed by Nginx, covering everything from CI/CD configuration to health checks, traffic shifting, and observability.
Planning the Deployment Pipeline
Before you write a single line of Dockerfile, map out the stages your code will travel through. A clear pipeline reduces manual steps and makes rollback trivial.
Choose the right CI/CD tool
- GitHub Actions – native, cheap, great for open‑source.
- GitLab CI – built‑in container registry, easy variable management.
- CircleCI – fast parallel jobs, excellent Docker layer caching.
- Jenkins – highly customizable if you already have an on‑premise setup.
Pick one that integrates with your source control and can push images to a registry your production hosts can pull from.
Containerizing the Application
A well‑crafted Docker image is the foundation of a smooth rollout.
Dockerfile best practices
# Use an official lightweight base image
FROM node:20-alpine AS builder
WORKDIR /app
# Install only production dependencies first for caching
COPY package*.json ./
RUN npm ci --only=production
# Copy source and build
COPY . .
RUN npm run build
# Runtime stage – smallest possible image
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
Key takeaways:
- Multi‑stage builds keep the final image lean.
- Separate dependency install from source copy to leverage Docker cache.
- Expose only the needed port and avoid running as root.
Nginx as a Reverse Proxy for Zero‑Downtime
Nginx sits in front of your containers, handling TLS termination, load‑balancing, and graceful failover.
Upstream configuration
# /etc/nginx/conf.d/upstream.conf
upstream app_backend {
# The "least_conn" strategy spreads traffic evenly.
least_conn;
server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
server 127.0.0.1:8082 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
When you spin up a new container on a fresh port (e.g., 8083), simply add it to the upstream block and reload Nginx (nginx -s reload
). Nginx will start routing new connections to the fresh instance while existing connections finish on the old one.
Blue‑Green Deployment Strategy
Blue‑green keeps two identical environments – blue (current) and green (next). Traffic is switched at the proxy level once the green stack passes health checks.
Steps Checklist
-
Build & push a new Docker image with a unique tag (e.g.,
v1.2.3
). - Deploy green containers on a separate port range (e.g., 8090‑8099).
- Run integration smoke tests against the green endpoint.
- Update Nginx upstream to point to green servers.
- Reload Nginx – traffic now flows to green.
- Monitor for errors; if any, rollback by pointing upstream back to blue.
- Retire blue containers after a safe cooldown period.
Automate steps 2‑5 with a CI job to eliminate human error.
Rolling Updates with Docker Compose
If you prefer a single compose file over two separate stacks, Docker Compose can perform rolling updates using the --scale
flag.
Example docker-compose.yml
version: "3.9"
services:
web:
image: myregistry.com/app:${APP_TAG}
deploy:
replicas: 4
update_config:
parallelism: 1
delay: 10s
order: start-first
ports:
- "8080:3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
Running docker compose up -d --pull always
will pull the new tag, spin up a fresh replica, wait for its health check, then gracefully stop the oldest replica. This gives you a zero‑downtime rollout without a separate Nginx reload step, but you lose the explicit blue‑green separation.
Health Checks and Traffic Shifting
Both Docker and Nginx rely on health checks to decide when a container is ready.
Dockerfile healthcheck example
HEALTHCHECK --interval=15s --timeout=3s \
CMD curl -f http://localhost:3000/health || exit 1
Nginx active health checks (optional module)
If you compile Nginx with the ngx_http_upstream_check_module
, you can enable active probing:
upstream app_backend {
server 127.0.0.1:8081;
server 127.0.0.1:8082;
check interval=3000 rise=2 fall=5 timeout=1000 type=http;
check_http_send "GET /health HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx;
}
Active checks give you immediate feedback, allowing the CI job to pause the rollout until all backends report healthy.
Observability & Logging
Zero‑downtime is only valuable if you can see when something goes wrong.
- Structured logs – JSON output from your app, collected by Loki or Elasticsearch.
- Metrics – Prometheus exporter for request latency, error rates, and container restarts.
- Tracing – OpenTelemetry spans that cross the Nginx proxy.
- Alerting – PagerDuty or Opsgenie alerts on health‑check failures or high error percentages.
Integrate these tools into your CI pipeline so a failed deployment automatically creates a ticket.
Final Checklist
- [ ] Docker image built with multi‑stage Dockerfile and tagged uniquely.
- [ ] CI pipeline pushes image to a secure registry.
- [ ] Nginx config uses an upstream block with health‑check parameters.
- [ ] Blue‑green containers started on isolated ports.
- [ ] Smoke tests run against the green environment.
- [ ] Nginx reload performed only after health checks pass.
- [ ] Rollback plan documented and automated.
- [ ] Observability stack (logs, metrics, traces) is ingesting data from both blue and green.
- [ ] Post‑deploy verification includes latency and error‑rate thresholds.
Follow this checklist on every release and you’ll keep user traffic flowing while your code evolves.
If you need help shipping this, the team at https://ramerlabs.com can help.
Top comments (0)