Introduction
Zero‑downtime deployments are a non‑negotiable expectation for modern SaaS products. As a DevOps lead, you’ve probably wrestled with traffic spikes, rolling back broken releases, and the dreaded "service unavailable" page. This checklist walks you through a pragmatic, Docker‑centric workflow that pairs a lightweight Nginx reverse proxy with blue‑green deployment patterns. Follow each step, and you’ll keep your users blissfully unaware when you push new code.
Prerequisites
Before you start, make sure you have:
- A Linux host (Ubuntu 22.04+ recommended) with Docker Engine ≥ 20.10 and Docker Compose ≥ 2.0 installed.
- A domain name pointing to the host’s public IP.
- Basic familiarity with Nginx configuration syntax.
- A Git repository that builds a Docker image for your app (Node.js, Python, Go – any language works).
If any of these are missing, pause the checklist and get them in place. Skipping prerequisites is the fastest way to introduce friction later.
1️⃣ Prepare a Stable Base Image
-
Pin the base: Use an immutable tag (e.g.,
node:18-alpine
) instead oflatest
. - Run as non‑root: Add a low‑privilege user inside the Dockerfile.
-
Health checks: Declare a
HEALTHCHECK
instruction so Docker can detect a bad container.
FROM node:18-alpine
# Create app user
RUN addgroup -S app && adduser -S -G app app
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Run as non‑root
USER app
# Health check – adjust path to your health endpoint
HEALTHCHECK --interval=30s --timeout=5s \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
2️⃣ Build and Tag Images Atomically
When you push a new version, tag it with the Git SHA. This eliminates ambiguity during rollouts.
# Build and tag with the current commit hash
COMMIT=$(git rev-parse --short HEAD)
docker build -t myapp:$COMMIT .
# Push to your registry (Docker Hub, ECR, GCR, etc.)
docker push myregistry.com/myapp:$COMMIT
3️⃣ Set Up Nginx as a Smart Reverse Proxy
Nginx will sit in front of two upstream groups: blue
and green
. Only one group receives traffic at a time.
# /etc/nginx/conf.d/app.conf
upstream blue {
server 127.0.0.1:8001; # Docker container on host port
}
upstream green {
server 127.0.0.1:8002;
}
# The active upstream is controlled via a variable
map $http_x_deploy_target $upstream {
default blue;
"green" green;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://$upstream;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Reload Nginx after any change:
sudo nginx -s reload
4️⃣ Docker‑Compose Blueprint for Blue‑Green
Keep both environments alive in the same compose file but expose them on different host ports.
version: "3.9"
services:
app-blue:
image: myregistry.com/myapp:${BLUE_TAG:-latest}
container_name: app_blue
ports:
- "8001:3000"
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
app-green:
image: myregistry.com/myapp:${GREEN_TAG:-latest}
container_name: app_green
ports:
- "8002:3000"
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
Tip: Keep the unused service stopped to save resources. You can docker compose stop app-green
when blue is live.
5️⃣ Deploy Workflow Checklist
✅ Step | What to Verify |
---|---|
1. Pull latest images |
docker compose pull pulls both blue and green tags. |
2. Update the standby tag | Export GREEN_TAG=$COMMIT (or BLUE_TAG if green is live). |
3. Start the standby container |
docker compose up -d app-green (or app-blue ). |
4. Wait for health |
docker inspect --format='{{json .State.Health}}' $(docker ps -qf "name=app-green") should show healthy . |
5. Switch Nginx upstream |
curl -X POST -H "X-Deploy-Target: green" http://localhost/switch (custom endpoint) or edit the X-Deploy-Target header in your load‑balancer. |
6. Verify traffic |
curl -I http://example.com should return 200 and the Server header from the new container. |
7. Drain old containers |
docker compose stop app-blue after confirming no 5xx errors. |
8. Tag promotion | Promote the new tag to latest in your registry if you want a stable fallback. |
6️⃣ Observability & Rollback
-
Metrics: Export container health to Prometheus via the
cAdvisor
exporter. -
Logs: Ship Nginx access logs and app stdout to Loki or ELK. Include the
$upstream
variable in logs to trace which version served a request. - Rollback: If the new version spikes error rates, repeat step 5 but point the header back to the previous upstream, then stop the problematic container.
7️⃣ Automate with CI/CD
Most teams embed the checklist into a pipeline. Below is a minimal GitHub Actions job that performs steps 1‑4 and leaves the manual switch for a post‑deployment approval.
name: Deploy Blue‑Green
on:
push:
branches: [main]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to registry
uses: docker/login-action@v2
with:
registry: myregistry.com
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASS }}
- name: Build & push image
env:
COMMIT: ${{ github.sha }}
run: |
docker build -t myregistry.com/myapp:${COMMIT} .
docker push myregistry.com/myapp:${COMMIT}
- name: Deploy standby
env:
GREEN_TAG: ${{ github.sha }}
run: |
docker compose pull
docker compose up -d app-green
After the workflow finishes, a senior engineer can trigger the Nginx switch via a secure endpoint or a simple ssh
command.
Conclusion
Zero‑downtime deployments don’t require exotic tooling; they need a disciplined checklist, immutable Docker images, and a proxy that can route traffic atomically. By keeping blue and green environments isolated, health‑checking rigorously, and observability wired from day one, you protect both your users and your team’s sanity.
If you need help shipping this, the team at https://ramerlabs.com can help.
Top comments (0)