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]
: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.
Reach out to KEYDAL for containerizing existing applications and production deployment with Compose. Contact us