Docker Compose is the standard way to run multiple containers with a single command and describe their networks, volumes and depends-on relationships in one file. It is indispensable at every stage from development to production. This guide covers setting up a typical web application stack.

Basic compose.yml

Related guides: What is DNS, settings · Domain names & WHOIS lookup · Hosting types guide · Nginx configuration · Plesk panel guide

# compose.yml (docker-compose.yml also works)
services:
  app:
    build: .
    ports: ['3000:3000']
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://app:secret@db:5432/appdb
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD', 'pg_isready', '-U', 'app']
      interval: 5s
      retries: 10
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports: ['80:80', '443:443']
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/letsencrypt:ro
    depends_on: [app]
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

Essential Commands

# Build + start (detached)
docker compose up -d --build

# Tail logs
docker compose logs -f app
docker compose logs --since 10m

# Restart a single service
docker compose restart app

# Rebuild a service and start it
docker compose up -d --build app

# Status
docker compose ps

# Shut everything down
docker compose down
# Remove volumes too (data is gone!)
docker compose down -v

Environment Files

Do not leave sensitive data in compose.yml. Use a .env file — Compose reads it automatically.

# compose.yml
services:
  app:
    environment:
      DATABASE_URL: ${DATABASE_URL}
      JWT_SECRET: ${JWT_SECRET}

# .env (git-ignore!)
DATABASE_URL=postgres://...
JWT_SECRET=supersecret

# Use a different env file
docker compose --env-file .env.production up -d

Network Management

By default, Compose creates a project-specific bridge network and services find each other by service name (postgres://db:5432). You can define custom networks for isolation.

services:
  frontend:
    networks: [public]
  backend:
    networks: [public, internal]
  db:
    networks: [internal]  # isolated from the outside world

networks:
  public:
  internal:
    internal: true  # no internet access

Volume Types

  • Named volume: pgdata:/var/lib/postgresql/data — managed by Docker, portable
  • Bind mount: ./config:/etc/app — host directory, ideal for development
  • tmpfs: type: tmpfs — RAM only, good for sensitive temp data
services:
  app:
    volumes:
      - ./src:/app/src           # bind (hot reload)
      - node_modules:/app/node_modules  # named
      - type: tmpfs
        target: /tmp

Production Best Practices

services:
  app:
    image: ghcr.io/user/app:v1.2.3  # pinned tag, not :latest
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          memory: 128M
    logging:
      driver: json-file
      options:
        max-size: '10m'
        max-file: '5'
    read_only: true
    tmpfs: ['/tmp', '/var/run']
    security_opt:
      - no-new-privileges:true
    cap_drop: [ALL]
Warning
In production, always use pinned versions (:v1.2.3 or SHA) instead of :latest. Otherwise a restart can bring unexpected changes.

Override Files (Dev vs Prod)

# compose.yml (base)
services:
  app:
    image: myapp:latest
    restart: unless-stopped

# compose.override.yml (loaded by default, for dev)
services:
  app:
    build: .
    volumes: ['./src:/app/src']
    command: npm run dev

# compose.prod.yml
services:
  app:
    image: ghcr.io/user/app:v1.2.3
    environment:
      NODE_ENV: production

# Usage
docker compose up                             # loads the override automatically
docker compose -f compose.yml -f compose.prod.yml up -d

Healthcheck and depends_on

depends_on only guarantees startup order, not that the service is ready. For actual readiness, use condition: service_healthy combined with a healthcheck.

services:
  app:
    depends_on:
      db:
        condition: service_healthy
  db:
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U app']
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s

Profiles

services:
  app: {}
  db: {}
  monitoring:
    image: prom/prometheus
    profiles: [monitoring]  # does not start by default

# Usage
docker compose up                          # app, db
docker compose --profile monitoring up     # + monitoring

Modern Web Hosting and Server Infrastructure

A performant web hosting service rests on three infrastructure decisions: NVMe SSD disks (4-6× IOPS over SATA SSD), LiteSpeed Web Server or Nginx + LSCache (9× request capacity over Apache) and CloudLinux + Imunify360 isolation. The hosting provider's control panel (cPanel, Plesk, DirectAdmin), daily backup policy, data center location and support response time make a big difference too. Turkish locations give low latency to local visitors, while Hetzner Frankfurt or OVH Roubaix suit global traffic. As your site grows, transitioning from shared hosting to VPS to dedicated server scales CPU/RAM/disk to your needs.

Conclusion

Docker Compose is the sweet spot for small and medium production where Kubernetes is overkill. You bring up the whole stack with one command and do migrations/deploys in minutes. Perfect for running 5-10 services on a single VPS.

Docker Compose setup

Reach out to KEYDAL for containerizing existing applications and production deployment with Compose. Contact us

WhatsApp