Docker Compose transforms multi-container application deployment from a series of manual docker run commands into a declarative, version-controlled configuration. A production-ready Compose deployment includes health checks (to ensure services are truly ready before dependent services start), resource limits (to prevent runaway containers from consuming all host resources), restart policies (to automatically recover from failures), and environment variable management. This guide covers building a production-grade deployment of a full-stack web application — Node.js API, PostgreSQL database, Redis cache, and Nginx reverse proxy — using Docker Compose on RHEL 9.

Prerequisites

  • Docker Engine and Docker Compose installed on RHEL 9

Step 1 — Application Dockerfile

# /var/www/myapp/Dockerfile — multi-stage Node.js build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]

Step 2 — Production compose.yaml

# /var/www/myapp/compose.yaml
services:
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped

  api:
    build: .
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://appuser:${DB_PASSWORD}@db:5432/appdb
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 20s
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"

  db:
    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
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

Step 3 — Deploy and Manage

# Set the database password in .env file
echo "DB_PASSWORD=SecurePass123!" > /var/www/myapp/.env

# Deploy the stack
cd /var/www/myapp
docker compose up -d --build

# Verify all services are healthy
docker compose ps

# Run database migrations (one-off command)
docker compose exec api node migrate.js

# Watch logs for errors on first deployment
docker compose logs -f --tail=50

Step 4 — Rolling Update Deployment

# Update and redeploy with zero downtime
git pull origin main

# Rebuild and recreate only changed services
docker compose up -d --build --no-deps api

# Verify the new version is running
docker compose ps
docker compose exec api node --version

Conclusion

Docker Compose on RHEL 9 with health checks, resource limits, and restart policies provides a production-ready single-server deployment that handles service startup ordering, automatic recovery from crashes, and resource isolation. The restart: unless-stopped policy (preferred over always) restarts containers that crash but respects manual docker compose stop commands, preventing unwanted restarts after intentional shutdowns. For high availability across multiple servers, transition to Docker Swarm or Kubernetes.

Next steps: How to Install Docker Engine on RHEL 9, How to Use Docker Volumes on RHEL 9, and How to Install Kubernetes on RHEL 9.