hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Deploying Node.js Apps with Docker

Why Docker

  • The "Works on My Machine" Problem
  • Docker Concepts

Your First Dockerfile

  • Writing a Dockerfile
  • Multi-Stage Builds
  • The .dockerignore File

Running Containers

  • Running and Managing Containers
  • Environment Variables and Secrets
  • Health Checks

Multi-Container Apps

  • Docker Compose
  • Adding a Reverse Proxy
  • Persistent Data

Deploying to Production

  • Deploying to a VPS
  • HTTPS with Let's Encrypt
  • Zero-Downtime Deploys

CI/CD

  • Building Images in CI
  • Automated Deployment

Production Hardening

  • Container Security
  • Logging and Monitoring
  • Deployment Checklist and Capstone

Zero-Downtime Deploys

The naive deploy

docker compose down
docker compose pull
docker compose up -d

This works but has downtime: between down and up, the app is unreachable. For a deploy that takes 30 seconds, users see errors for 30 seconds.

The zero-downtime approach

Start the new container before stopping the old one. Only stop the old one after the new one is healthy.

#!/bin/bash
# deploy.sh

set -e

IMAGE="yourusername/myapp:latest"

echo "Pulling new image..."
docker pull $IMAGE

echo "Starting new container..."
docker run -d \
  --name myapp-new \
  --network app_default \
  -e NODE_ENV=production \
  --env-file .env.production \
  -v app-data:/data \
  $IMAGE

echo "Waiting for health check..."
for i in {1..30}; do
  STATUS=$(docker inspect --format='{{.State.Health.Status}}' myapp-new 2>/dev/null || echo "starting")
  if [ "$STATUS" = "healthy" ]; then
    echo "New container is healthy!"
    break
  fi
  if [ "$i" -eq 30 ]; then
    echo "Health check failed. Rolling back."
    docker rm -f myapp-new
    exit 1
  fi
  sleep 2
done

echo "Swapping containers..."
# Update Nginx to point to the new container
# (or rename containers)
docker stop myapp
docker rm myapp
docker rename myapp-new myapp

echo "Deploy complete!"

The old container serves traffic while the new one starts. After the new one passes health checks, traffic switches over. The old container is stopped.

With Docker Compose

Docker Compose handles this more gracefully with --no-deps:

#!/bin/bash
# deploy-compose.sh

set -e

echo "Pulling new image..."
docker compose pull app

echo "Recreating app container..."
docker compose up -d --no-deps --build app

echo "Waiting for health..."
sleep 10  # Wait for health check to pass

echo "Checking health..."
docker compose ps app
# Verify status shows (healthy)

echo "Deploy complete!"

--no-deps recreates only the app service without restarting Nginx or other services. Docker Compose starts the new container and stops the old one with a brief overlap.

[!NOTE] Docker Compose’s default behavior has a small window where neither container is running. For truly zero-downtime deploys with Docker Compose, use docker compose up -d --scale app=2 to run two instances, then scale back down. Or use the manual container approach shown above.

Rollback

If the new container fails health checks, roll back by keeping the old container:

# In the deploy script, on health check failure:
echo "Rolling back..."
docker rm -f myapp-new
# The old container (myapp) is still running — no downtime

The old container was never stopped. The failed deployment has zero impact on users.

Tagging for rollback

Tag images with version numbers, not just latest:

docker build -t yourusername/myapp:1.2.3 -t yourusername/myapp:latest .
docker push yourusername/myapp:1.2.3
docker push yourusername/myapp:latest

If version 1.2.3 has a bug, roll back to 1.2.2:

docker pull yourusername/myapp:1.2.2
docker run ... yourusername/myapp:1.2.2

latest is a convenience but not a rollback mechanism. Specific version tags are.

Exercises

Exercise 1: Write the deploy script. Deploy a new version while the old one is running. Verify there was no downtime.

Exercise 2: Deploy a broken image (one that fails health checks). Verify the script rolls back and the old container keeps serving traffic.

Exercise 3: Tag your images with version numbers. Deploy version 2. Roll back to version 1.

Why do we wait for the health check before stopping the old container?

← HTTPS with Let's Encrypt Building Images in CI →

© 2026 hectoday. All rights reserved.