Back to blog

Docker Compose for Production: Secure and Scalable Deployment Configuration

Docker Compose is famous for simplifying local development environments. However, with the right configurations, Docker Compose is also a highly effective, lightweight orchestration tool for single-host production deployments.

Deploying to production requires a shift in how you configure containers. You must prioritize security, resource constraints, persistent storage, and automatic recovery from system crashes.

In this guide, we will explore production-grade Docker Compose patterns, cover security hardening techniques, and write a secure, resource-constrained docker-compose.yml template.

Production Best Practices Checklist

When moving from local development to production, check your Compose configuration against these rules:

1. Lock Specific Image Tags

Never use the latest tag in production. The latest tag points to whatever build was pushed most recently, which can introduce breaking changes during automated deployments. Always pin your images to specific semantic version tags.

# Avoid
image: node:latest

# Use
image: node:20.11.0-alpine

2. Impose Resource Constraints

By default, a container can consume all available host CPU and memory, potentially starving other essential services (like your database or web proxy). Always define explicit resource limits.

deploy:
  resources:
    limits:
      cpus: '0.50'
      memory: 512M
    reservations:
      cpus: '0.25'
      memory: 256M

3. Configure Restart Policies

Containers can crash due to code exceptions or host system restarts. Ensure your services recover automatically using the unless-stopped restart policy.

restart: unless-stopped

This ensures the container starts automatically when the host server reboots, unless you manually executed docker compose stop.

4. Run as Non-Root Users

By default, Docker containers execute processes as the host root user. If your application has a remote code execution vulnerability, attackers can gain root access to the entire host server.

Always write your Dockerfiles to switch to a non-root user (e.g., node, alpine, or custom uid), and ensure the container runtime runs in read-only mode where possible:

read_only: true
security_opt:
  - no-new-privileges:true

A Production-Ready Docker Compose Template

Here is a secure, production-grade Docker Compose configuration featuring a Node.js web application and a PostgreSQL database.

Create a file called docker-compose.prod.yml:

version: '3.8'

services:
  web:
    image: myapp/node-app:1.4.2-alpine
    container_name: prod-web-app
    restart: unless-stopped
    read_only: true
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp
    environment:
      NODE_ENV: production
      DATABASE_URL: postgresql://db_user:${DB_PASSWORD}@db:5432/prod_db
    ports:
      - "127.0.0.1:3000:3000"
    depends_on:
      db:
        condition: service_healthy
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1024M
        reservations:
          cpus: '0.5'
          memory: 512M

  db:
    image: postgres:16.1-alpine
    container_name: prod-database
    restart: unless-stopped
    environment:
      POSTGRES_USER: db_user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: prod_db
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "127.0.0.1:5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U db_user -d prod_db"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2048M

volumes:
  pgdata:
    driver: local

Crucial Configurations Explained

  • Ports Binding: Instead of mapping ports to "3000:3000", the template binds to "127.0.0.1:3000:3000". This restricts container ports to the local loopback interface, meaning external traffic cannot access the containers directly. Access is routed securely through a reverse proxy (like Nginx) running on the host.
  • Service Healthchecks: The web application relies on the database being fully ready to accept connections. We define a native healthcheck on the database service and configure the web app's depends_on block with condition: service_healthy to guarantee correct startup order.
  • Environment Secrets: Secrets like ${DB_PASSWORD} are not hardcoded. Docker Compose automatically resolves these variables from a local .env file on the host machine during startup.

Conclusion

Docker Compose provides a robust, low-complexity deployment pipeline for single-instance production setups. By locking image tags, restricting CPU and memory utilization, locking down execution permissions to non-root accounts, and binding ports locally, you can deploy applications securely without needing the operational complexity of Kubernetes.