ในบทความนี้
- Docker Compose คืออะไร?
- ตัวอย่างแรก — Web App + Database
- Full Stack — React + API + DB + Redis + Nginx
- 🔧 Docker Compose คำสั่งสำคัญ
- 📁 .env File — แยก Config ออกจาก YAML
- 🔀 Multi-Environment — Dev vs Production
- 💾 Volumes — เก็บข้อมูลถาวร
- 🌐 Networks — Container คุยกันยังไง
- ❤️ Health Checks — ตรวจสุขภาพ
- 🚀 Production Tips
- สรุป
In this post
- What Is Docker Compose?
- A First Example — Web App + Database
- Full Stack — React + API + DB + Redis + Nginx
- 🔧 The Docker Compose Commands That Matter
- 📁 .env Files — Config Out of the YAML
- 🔀 Multi-Environment — Dev vs Production
- 💾 Volumes — Data That Persists
- 🌐 Networks — How Containers Talk
- ❤️ Health Checks — Taking the Pulse
- 🚀 Production Tips
- Summary
ในโพสต์ Docker vs VMs เราเรียนรู้ว่า Docker สร้าง container ได้ง่าย แต่ app จริงมักใช้หลาย services — web server, API, database, cache, queue...
ถ้าต้อง docker run ทีละตัว ตั้ง network เอง ตั้ง volume เอง... จะบ้าตาย! 😵
Docker Compose แก้ปัญหานี้ — กำหนดทุก service ในไฟล์ YAML เดียว แล้วรันด้วยคำสั่งเดียว! 🐳🐕
Docker Compose คืออะไร?
Docker Compose คือเครื่องมือที่ช่วย กำหนด + รันหลาย containers พร้อมกัน ด้วยไฟล์ docker-compose.yml (หรือ compose.yml)
💡 จำง่ายๆ: Dockerfile = สูตรทำ 1 container | Docker Compose = สูตรทำทั้งร้านอาหาร (หลาย containers)
ตัวอย่างแรก — Web App + Database
มาเริ่มจากตัวอย่างง่ายที่สุด — web app + PostgreSQL database แค่ 2 services แต่ถ้าไม่มี Compose ต้อง: สร้าง network เอง, รัน postgres container ตั้ง password + volume, รัน app container ตั้ง env vars + เชื่อม network... รวม 5-6 คำสั่ง! Docker Compose ทำให้เหลือแค่ docker compose up:
# docker-compose.yml
services:
# Web Application
app:
build: . # build จาก Dockerfile ใน directory นี้
ports:
- "3000:3000" # host:container
environment:
- DATABASE_URL=postgres://myuser:mypass@db:5432/mydb
- NODE_ENV=development
volumes:
- .:/app # mount code เข้าไป (dev mode)
- /app/node_modules # ไม่ mount node_modules
depends_on:
db:
condition: service_healthy
restart: unless-stopped
# PostgreSQL Database
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=myuser
- POSTGRES_PASSWORD=mypass
- POSTGRES_DB=mydb
volumes:
- pgdata:/var/lib/postgresql/data # persist data
ports:
- "5432:5432"
healthcheck:
test: pg_isready -U myuser
interval: 10s
retries: 5
volumes:
pgdata: # named volume สำหรับ DB data
# รันทุก service
docker compose up -d
# [+] Running 3/3
# ✔ Network myapp_default Created
# ✔ Container myapp-db-1 Started
# ✔ Container myapp-app-1 Started
# ดูสถานะ
docker compose ps
# NAME SERVICE STATUS PORTS
# myapp-app-1 app running 0.0.0.0:3000->3000/tcp
# myapp-db-1 db running 0.0.0.0:5432->5432/tcp
# ดู logs
docker compose logs -f app # follow logs ของ app
docker compose logs # logs ทุก service
# หยุดทุก service
docker compose down # หยุด + ลบ containers
docker compose down -v # + ลบ volumes ด้วย (⚠️ data หาย!)
Full Stack — React + API + DB + Redis + Nginx
ตัวอย่างจริงที่ใกล้ production — 5 services ทำงานร่วมกัน: React frontend, Node.js API, PostgreSQL database, Redis cache, และ Nginx reverse proxy สังเกตการใช้ depends_on กำหนดลำดับการ start, healthcheck ตรวจว่า service พร้อมจริงก่อน start ตัวที่พึ่ง, volumes สำหรับ persistent data, และ networks แยก frontend/backend traffic:
# docker-compose.yml — Full Stack
services:
# ─── Nginx Reverse Proxy ───
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- frontend
- backend
restart: always
# ─── React Frontend ───
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
environment:
- REACT_APP_API_URL=/api
expose:
- "3000" # expose ให้ nginx เห็น (ไม่เปิด host)
# ─── Node.js Backend ───
backend:
build:
context: ./backend
dockerfile: Dockerfile
environment:
- DATABASE_URL=postgres://app:secret@db:5432/appdb
- REDIS_URL=redis://redis:6379
- JWT_SECRET=${JWT_SECRET} # อ่านจาก .env file
expose:
- "8080"
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
# ─── PostgreSQL ───
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=appdb
volumes:
- pgdata:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: pg_isready -U app
interval: 10s
retries: 5
restart: unless-stopped
# ─── Redis Cache ───
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redisdata:/data
restart: unless-stopped
volumes:
pgdata:
redisdata:
Nginx Config
Nginx ทำหน้าที่เป็น reverse proxy — รับ traffic ทั้งหมดแล้วส่งต่อไปยัง frontend หรือ API ตาม URL path ข้อดีคือ expose port เดียว (80) แทนที่ต้องเปิดหลาย ports และสามารถ cache, gzip, load balance ได้:
# nginx/default.conf
upstream frontend {
server frontend:3000;
}
upstream backend {
server backend:8080;
}
server {
listen 80;
# Frontend
location / {
proxy_pass http://frontend;
proxy_set_header Host $host;
}
# API → Backend
location /api/ {
proxy_pass http://backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# WebSocket support
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
🔧 Docker Compose คำสั่งสำคัญ
คำสั่ง Docker Compose ที่ใช้บ่อยในชีวิตประจำวัน — ตั้งแต่ start/stop services, ดู logs, exec เข้าไปใน container, ไปจนถึง rebuild images หลังแก้ Dockerfile สังเกตว่า docker compose (ไม่มี hyphen) เป็น v2 ที่แนะนำ แทน docker-compose v1:
# ─── Lifecycle ───
docker compose up # รัน (foreground)
docker compose up -d # รัน (background) ⭐
docker compose down # หยุด + ลบ containers
docker compose down -v # + ลบ volumes
docker compose restart # restart ทุก service
docker compose stop # หยุด (ไม่ลบ)
docker compose start # เริ่มจาก stop
# ─── Build ───
docker compose build # build ทุก service
docker compose build --no-cache # build ใหม่ ไม่ใช้ cache
docker compose up --build # build แล้วรันเลย ⭐
# ─── Status & Logs ───
docker compose ps # ดูสถานะ
docker compose logs # ดู logs ทั้งหมด
docker compose logs -f backend # follow logs ของ backend
docker compose top # ดู processes ในแต่ละ container
# ─── Execute ───
docker compose exec backend sh # เข้าไปใน container ⭐
docker compose exec db psql -U app appdb # เข้า PostgreSQL
docker compose run --rm backend npm test # รันแล้วลบ container
# ─── Scale ───
docker compose up -d --scale backend=3 # รัน backend 3 ตัว!
📁 .env File — แยก Config ออกจาก YAML
.env file ช่วยแยก sensitive config (passwords, API keys) ออกจาก docker-compose.yml — ทำให้สามารถ commit compose file เข้า Git ได้โดยไม่ leak secrets เพราะ .env อยู่ใน .gitignore Docker Compose อ่าน .env อัตโนมัติ:
# .env (อยู่ข้างๆ docker-compose.yml)
POSTGRES_USER=app
POSTGRES_PASSWORD=supersecret
POSTGRES_DB=myapp
JWT_SECRET=my-jwt-secret-key
NODE_ENV=development
APP_PORT=3000
# docker-compose.yml — ใช้ตัวแปรจาก .env
services:
backend:
build: ./backend
ports:
- "${APP_PORT}:${APP_PORT}"
environment:
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
- JWT_SECRET=${JWT_SECRET}
- NODE_ENV=${NODE_ENV}
- PORT=${APP_PORT}
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
🐕 สำคัญ! อย่าลืมใส่.envใน.gitignore— ไม่ commit secrets เข้า Git!
🔀 Multi-Environment — Dev vs Production
Dev environment ต้องการ hot-reload, debug tools, volume mounts สำหรับ live coding ส่วน production ต้อง optimize image size, set resource limits, ปิด debug mode จะจัดการ config ต่างๆ ยังไงให้ไม่ต้อง maintain 2 ไฟล์ที่ซ้ำกัน? Docker Compose มี 2 วิธีหลัก:
วิธีที่ 1: Override Files
Override files คือการแยก base config (docker-compose.yml) กับ environment-specific config (docker-compose.override.yml หรือ docker-compose.prod.yml) Docker Compose จะ merge ทั้ง 2 ไฟล์เข้าด้วยกัน — base กำหนด services ทั้งหมด, override เพิ่ม/แก้เฉพาะค่าที่ต่างกัน:
# docker-compose.yml (base — shared config)
services:
backend:
build: ./backend
environment:
- DATABASE_URL=postgres://app:pass@db:5432/mydb
depends_on:
- db
db:
image: postgres:16-alpine
# docker-compose.override.yml (dev — auto-loaded!)
services:
backend:
volumes:
- ./backend:/app # hot reload
ports:
- "8080:8080"
- "9229:9229" # debug port
environment:
- NODE_ENV=development
command: npm run dev
# docker-compose.prod.yml (production)
services:
backend:
restart: always
environment:
- NODE_ENV=production
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
# Development (auto-loads override)
docker compose up
# Production (explicit file)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
วิธีที่ 2: Profiles
Profiles (Docker Compose v2) ช่วยให้ group services ตาม use case — เช่น debug tools, monitoring stack, seeding scripts ที่ไม่ต้องรันทุกครั้ง แค่ docker compose --profile debug up จะ start เฉพาะ services ที่ tag ด้วย profile "debug":
services:
backend:
build: ./backend
# ไม่มี profile = รันเสมอ
db:
image: postgres:16-alpine
# ไม่มี profile = รันเสมอ
# Dev tools — รันเฉพาะ dev
adminer:
image: adminer
ports:
- "8888:8080"
profiles:
- dev # รันเมื่อ --profile dev
mailhog:
image: mailhog/mailhog
ports:
- "8025:8025"
profiles:
- dev
# Monitoring — รันเฉพาะ monitoring
prometheus:
image: prom/prometheus
profiles:
- monitoring
# รันแค่ base services
docker compose up -d
# รัน + dev tools
docker compose --profile dev up -d
# รัน + monitoring
docker compose --profile monitoring up -d
# รันทั้งหมด
docker compose --profile dev --profile monitoring up -d
💾 Volumes — เก็บข้อมูลถาวร
โดย default เมื่อ container หยุด ข้อมูลข้างในหายหมด! Volumes แก้ปัญหานี้โดยเก็บข้อมูลไว้นอก container — database data, uploaded files, logs จะไม่หายแม้ docker compose down มี 2 แบบ: named volumes (Docker จัดการ path ให้ เหมาะกับ data) และ bind mounts (mount directory จาก host เข้า container เหมาะกับ development):
services:
db:
image: postgres:16-alpine
volumes:
# Named volume — Docker จัดการให้ (production)
- pgdata:/var/lib/postgresql/data
# Bind mount — map folder จาก host (dev)
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
backend:
build: ./backend
volumes:
# Bind mount — hot reload สำหรับ dev
- ./backend/src:/app/src
# Anonymous volume — ป้องกัน overwrite
- /app/node_modules
volumes:
pgdata: # ประกาศ named volume
driver: local
# ดู volumes
docker volume ls
# ลบ volumes ที่ไม่ใช้
docker volume prune
# Backup database
docker compose exec db pg_dump -U app mydb > backup.sql
# Restore database
cat backup.sql | docker compose exec -T db psql -U app mydb
🌐 Networks — Container คุยกันยังไง
Docker Compose สร้าง default network ให้ทุก service อัตโนมัติ — containers สื่อสารกันโดยใช้ service name เป็น hostname เช่น API container เรียก database ด้วย postgres://db:5432 ไม่ต้องใช้ IP address สำหรับ security ที่ดีขึ้น ควรสร้าง networks แยก — frontend network เฉพาะ web+nginx, backend network เฉพาะ API+DB ทำให้ frontend เข้าถึง DB โดยตรงไม่ได้:
services:
frontend:
networks:
- frontend-net # เข้าถึงได้จาก nginx
backend:
networks:
- frontend-net # nginx เข้าถึงได้
- backend-net # เข้าถึง db ได้
db:
networks:
- backend-net # เฉพาะ backend เข้าถึง ✅
# frontend เข้าไม่ได้ ✅ (security!)
networks:
frontend-net:
backend-net:
💡 Security Tip: แยก network เพื่อ จำกัดการเข้าถึง — frontend ไม่ควรเข้า DB ตรงได้!
❤️ Health Checks — ตรวจสุขภาพ
Health checks ตรวจว่า service ทำงานจริง ไม่ใช่แค่ "process ยังรันอยู่" — เช่น PostgreSQL container อาจ start แล้วแต่ยังรับ connection ไม่ได้ ถ้า API start ก่อน DB พร้อม จะ crash! depends_on + condition: service_healthy แก้ปัญหานี้โดย wait จน health check ผ่านก่อนค่อย start service ที่พึ่ง:
services:
backend:
build: ./backend
healthcheck:
test: curl -f http://localhost:8080/health || exit 1
interval: 30s # ตรวจทุก 30 วินาที
timeout: 10s # timeout 10 วินาที
retries: 3 # ลองใหม่ 3 ครั้ง
start_period: 40s # รอ 40 วินาทีก่อนเริ่มตรวจ
depends_on:
db:
condition: service_healthy # รอจน db healthy ก่อน!
db:
image: postgres:16-alpine
healthcheck:
test: pg_isready -U app
interval: 10s
retries: 5
redis:
image: redis:7-alpine
healthcheck:
test: redis-cli ping
interval: 10s
retries: 3
🚀 Production Tips
Docker Compose ใช้ใน production ได้สำหรับ small-medium apps (ถ้ายังไม่ต้องการ Kubernetes) แต่ต้องตั้งค่าให้ production-ready — restart policies, resource limits, logging, security hardening เคล็ดลับสำคัญ:
# docker-compose.prod.yml
services:
backend:
image: myregistry.com/myapp:v2.1.0 # ใช้ image จาก registry (ไม่ build)
restart: always
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
reservations:
memory: 256M
logging:
driver: json-file
options:
max-size: "10m" # จำกัดขนาด log file
max-file: "3" # เก็บแค่ 3 ไฟล์
read_only: true # filesystem read-only (security)
tmpfs:
- /tmp # ให้เขียนได้แค่ /tmp
security_opt:
- no-new-privileges:true
Docker Compose vs Kubernetes
คำถามที่พบบ่อย — "ควรใช้ Docker Compose หรือ Kubernetes?" คำตอบง่ายๆ: Compose สำหรับ single-host (1 server, dev environment, small apps), Kubernetes สำหรับ multi-host (หลาย servers, auto-scaling, high availability, large teams) เริ่มด้วย Compose แล้วย้ายไป K8s เมื่อ scale จนจำเป็น:
| Feature | Docker Compose | Kubernetes |
|---|---|---|
| เหมาะกับ | Dev, small production | Large-scale production |
| เครื่อง | เครื่องเดียว | หลายเครื่อง (cluster) |
| Auto-scaling | ❌ Manual scale | ✅ HPA auto-scale |
| Self-healing | ⚠️ restart policy | ✅ Full self-healing |
| Rolling update | ⚠️ Basic | ✅ Zero-downtime |
| ความซับซ้อน | ง่าย (1 YAML file) | ซับซ้อน (many manifests) |
| Learning curve | 30 นาที | 30 วัน+ |
💡 กฎง่ายๆ: เครื่องเดียว + ทีมเล็ก = Docker Compose | หลายเครื่อง + scale = Kubernetes
สรุป
Docker Compose เป็นเครื่องมือที่ทุก developer ควรรู้ — ทำให้การ setup development environment เหลือแค่ docker compose up ไม่ต้อง install dependencies บนเครื่อง ทุกคนในทีมได้ environment เดียวกัน "works on my machine" หมดไป:
- Docker Compose = จัดการหลาย containers ด้วย
docker-compose.ymlไฟล์เดียว - คำสั่งหลัก =
up -d(รัน),down(หยุด),logs -f(ดู log),exec(เข้า container) - .env file = แยก secrets/config ออกจาก YAML (อย่า commit!)
- Multi-env = Override files หรือ Profiles แยก dev/prod
- Volumes = Named volumes สำหรับ data, Bind mounts สำหรับ dev
- Networks = แยก network เพื่อ security (frontend ≠ database)
- Health checks = ตรวจสุขภาพ +
depends_on condition: service_healthy - Production = resource limits, logging, read-only filesystem 🐳🐕
In the Docker vs VMs post we learned that Docker makes a container easy to build — but a real app usually runs several services: web server, API, database, cache, queue...
Running docker run one at a time, wiring the network yourself, setting up the volumes yourself... you would lose your mind! 😵
Docker Compose solves exactly this — declare every service in a single YAML file, then start them all with one command! 🐳🐕
What Is Docker Compose?
Docker Compose is the tool that lets you declare and run many containers together from one docker-compose.yml file (or compose.yml).
💡 An easy way to remember it: a Dockerfile is the recipe for 1 container | Docker Compose is the recipe for the whole restaurant (many containers)
A First Example — Web App + Database
Let's start with the simplest possible example — a web app + a PostgreSQL database. Only 2 services, but without Compose you would have to: create the network yourself, run the postgres container with its password and volume, run the app container with its env vars and attach it to the network... 5-6 commands in all! Docker Compose reduces that to docker compose up:
# docker-compose.yml
services:
# Web Application
app:
build: . # build from the Dockerfile in this directory
ports:
- "3000:3000" # host:container
environment:
- DATABASE_URL=postgres://myuser:mypass@db:5432/mydb
- NODE_ENV=development
volumes:
- .:/app # mount the code in (dev mode)
- /app/node_modules # do not mount node_modules
depends_on:
db:
condition: service_healthy
restart: unless-stopped
# PostgreSQL Database
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=myuser
- POSTGRES_PASSWORD=mypass
- POSTGRES_DB=mydb
volumes:
- pgdata:/var/lib/postgresql/data # persist data
ports:
- "5432:5432"
healthcheck:
test: pg_isready -U myuser
interval: 10s
retries: 5
volumes:
pgdata: # named volume for the DB data
# run every service
docker compose up -d
# [+] Running 3/3
# ✔ Network myapp_default Created
# ✔ Container myapp-db-1 Started
# ✔ Container myapp-app-1 Started
# check the status
docker compose ps
# NAME SERVICE STATUS PORTS
# myapp-app-1 app running 0.0.0.0:3000->3000/tcp
# myapp-db-1 db running 0.0.0.0:5432->5432/tcp
# read the logs
docker compose logs -f app # follow the app's logs
docker compose logs # logs from every service
# stop every service
docker compose down # stop + remove the containers
docker compose down -v # + remove the volumes too (⚠️ data gone!)
Full Stack — React + API + DB + Redis + Nginx
A realistic example, close to production — 5 services working together: a React frontend, a Node.js API, a PostgreSQL database, a Redis cache, and an Nginx reverse proxy. Notice how depends_on sets the start order, healthcheck proves a service is genuinely ready before anything that depends on it starts, volumes hold the persistent data, and networks separate frontend from backend traffic:
# docker-compose.yml — Full Stack
services:
# ─── Nginx Reverse Proxy ───
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- frontend
- backend
restart: always
# ─── React Frontend ───
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
environment:
- REACT_APP_API_URL=/api
expose:
- "3000" # expose it to nginx (not published to the host)
# ─── Node.js Backend ───
backend:
build:
context: ./backend
dockerfile: Dockerfile
environment:
- DATABASE_URL=postgres://app:secret@db:5432/appdb
- REDIS_URL=redis://redis:6379
- JWT_SECRET=${JWT_SECRET} # read from the .env file
expose:
- "8080"
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
# ─── PostgreSQL ───
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=appdb
volumes:
- pgdata:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: pg_isready -U app
interval: 10s
retries: 5
restart: unless-stopped
# ─── Redis Cache ───
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redisdata:/data
restart: unless-stopped
volumes:
pgdata:
redisdata:
Nginx Config
Nginx acts as the reverse proxy — it takes all the traffic and forwards it to the frontend or the API according to the URL path. The benefit is that you expose a single port (80) instead of opening several, and you get caching, gzip and load balancing along the way:
# nginx/default.conf
upstream frontend {
server frontend:3000;
}
upstream backend {
server backend:8080;
}
server {
listen 80;
# Frontend
location / {
proxy_pass http://frontend;
proxy_set_header Host $host;
}
# API → Backend
location /api/ {
proxy_pass http://backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# WebSocket support
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
🔧 The Docker Compose Commands That Matter
The Docker Compose commands you reach for every day — starting and stopping services, reading logs, exec-ing into a container, rebuilding images after a Dockerfile change. Note that docker compose (no hyphen) is v2, the recommended form, in place of docker-compose v1:
# ─── Lifecycle ───
docker compose up # run (foreground)
docker compose up -d # run (background) ⭐
docker compose down # stop + remove the containers
docker compose down -v # + remove the volumes
docker compose restart # restart every service
docker compose stop # stop (without removing)
docker compose start # start again from stopped
# ─── Build ───
docker compose build # build every service
docker compose build --no-cache # rebuild, ignoring the cache
docker compose up --build # build, then run ⭐
# ─── Status & Logs ───
docker compose ps # status
docker compose logs # all the logs
docker compose logs -f backend # follow the backend's logs
docker compose top # processes inside each container
# ─── Execute ───
docker compose exec backend sh # get a shell in the container ⭐
docker compose exec db psql -U app appdb # into PostgreSQL
docker compose run --rm backend npm test # run, then remove the container
# ─── Scale ───
docker compose up -d --scale backend=3 # run 3 backends!
📁 .env Files — Config Out of the YAML
An .env file keeps the sensitive config (passwords, API keys) out of docker-compose.yml — which is what makes the compose file safe to commit to Git without leaking secrets, because .env lives in .gitignore. Docker Compose reads .env automatically:
# .env (sits next to docker-compose.yml)
POSTGRES_USER=app
POSTGRES_PASSWORD=supersecret
POSTGRES_DB=myapp
JWT_SECRET=my-jwt-secret-key
NODE_ENV=development
APP_PORT=3000
# docker-compose.yml — using the variables from .env
services:
backend:
build: ./backend
ports:
- "${APP_PORT}:${APP_PORT}"
environment:
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
- JWT_SECRET=${JWT_SECRET}
- NODE_ENV=${NODE_ENV}
- PORT=${APP_PORT}
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
🐕 Important! Do not forget to put.envin.gitignore— never commit secrets to Git!
🔀 Multi-Environment — Dev vs Production
A dev environment wants hot reload, debug tools and volume mounts for live coding. Production wants an optimized image size, resource limits and debug mode off. So how do you manage the differing config without maintaining two files that mostly repeat each other? Docker Compose offers two main approaches:
Approach 1: Override Files
Override files split the base config (docker-compose.yml) from the environment-specific config (docker-compose.override.yml or docker-compose.prod.yml). Docker Compose merges the two files — the base declares all the services, the override adds or changes only the values that differ:
# docker-compose.yml (base — shared config)
services:
backend:
build: ./backend
environment:
- DATABASE_URL=postgres://app:pass@db:5432/mydb
depends_on:
- db
db:
image: postgres:16-alpine
# docker-compose.override.yml (dev — auto-loaded!)
services:
backend:
volumes:
- ./backend:/app # hot reload
ports:
- "8080:8080"
- "9229:9229" # debug port
environment:
- NODE_ENV=development
command: npm run dev
# docker-compose.prod.yml (production)
services:
backend:
restart: always
environment:
- NODE_ENV=production
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
# Development (auto-loads the override)
docker compose up
# Production (explicit files)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Approach 2: Profiles
Profiles (Docker Compose v2) let you group services by use case — debug tools, a monitoring stack, seeding scripts, anything that need not run every time. A plain docker compose --profile debug up starts only the services tagged with the "debug" profile:
services:
backend:
build: ./backend
# no profile = always runs
db:
image: postgres:16-alpine
# no profile = always runs
# Dev tools — dev only
adminer:
image: adminer
ports:
- "8888:8080"
profiles:
- dev # runs with --profile dev
mailhog:
image: mailhog/mailhog
ports:
- "8025:8025"
profiles:
- dev
# Monitoring — monitoring only
prometheus:
image: prom/prometheus
profiles:
- monitoring
# just the base services
docker compose up -d
# base + dev tools
docker compose --profile dev up -d
# base + monitoring
docker compose --profile monitoring up -d
# everything
docker compose --profile dev --profile monitoring up -d
💾 Volumes — Data That Persists
By default, when a container stops everything inside it is gone! Volumes solve this by keeping the data outside the container — database data, uploaded files and logs all survive even a docker compose down. There are two kinds: named volumes (Docker manages the path for you, the right choice for data) and bind mounts (mount a directory from the host into the container, the right choice for development):
services:
db:
image: postgres:16-alpine
volumes:
# Named volume — Docker manages it (production)
- pgdata:/var/lib/postgresql/data
# Bind mount — map a folder from the host (dev)
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
backend:
build: ./backend
volumes:
# Bind mount — hot reload for dev
- ./backend/src:/app/src
# Anonymous volume — keeps it from being overwritten
- /app/node_modules
volumes:
pgdata: # declare the named volume
driver: local
# list the volumes
docker volume ls
# remove the unused volumes
docker volume prune
# Backup database
docker compose exec db pg_dump -U app mydb > backup.sql
# Restore database
cat backup.sql | docker compose exec -T db psql -U app mydb
🌐 Networks — How Containers Talk
Docker Compose creates a default network for every service automatically — containers reach each other by using the service name as the hostname. The API container talks to the database at postgres://db:5432, no IP address needed. For better security, create separate networks — a frontend network for web+nginx, a backend network for API+DB — so the frontend cannot reach the DB directly at all:
services:
frontend:
networks:
- frontend-net # reachable from nginx
backend:
networks:
- frontend-net # nginx can reach it
- backend-net # it can reach db
db:
networks:
- backend-net # backend only ✅
# frontend cannot ✅ (security!)
networks:
frontend-net:
backend-net:
💡 Security Tip: split the networks to limit what can reach what — the frontend has no business touching the DB directly!
❤️ Health Checks — Taking the Pulse
Health checks prove that a service is genuinely working, not merely that "the process is still running" — a PostgreSQL container may have started and still not be accepting connections. If the API starts before the DB is ready, it crashes! depends_on plus condition: service_healthy fixes this by waiting for the health check to pass before starting anything that depends on it:
services:
backend:
build: ./backend
healthcheck:
test: curl -f http://localhost:8080/health || exit 1
interval: 30s # check every 30 seconds
timeout: 10s # 10-second timeout
retries: 3 # retry 3 times
start_period: 40s # wait 40 seconds before checking at all
depends_on:
db:
condition: service_healthy # wait until db is healthy first!
db:
image: postgres:16-alpine
healthcheck:
test: pg_isready -U app
interval: 10s
retries: 5
redis:
image: redis:7-alpine
healthcheck:
test: redis-cli ping
interval: 10s
retries: 3
🚀 Production Tips
Docker Compose is usable in production for small and medium apps (when you do not yet need Kubernetes) — but it has to be configured production-ready: restart policies, resource limits, logging, security hardening. The tips that matter:
# docker-compose.prod.yml
services:
backend:
image: myregistry.com/myapp:v2.1.0 # use the image from the registry (no build)
restart: always
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
reservations:
memory: 256M
logging:
driver: json-file
options:
max-size: "10m" # cap the log file size
max-file: "3" # keep only 3 files
read_only: true # read-only filesystem (security)
tmpfs:
- /tmp # writable at /tmp only
security_opt:
- no-new-privileges:true
Docker Compose vs Kubernetes
The question that comes up constantly — "should I use Docker Compose or Kubernetes?" The simple answer: Compose is for a single host (1 server, dev environments, small apps), Kubernetes is for many hosts (several servers, auto-scaling, high availability, large teams). Start with Compose and move to K8s when the scale actually demands it:
| Feature | Docker Compose | Kubernetes |
|---|---|---|
| Best for | Dev, small production | Large-scale production |
| Machines | A single machine | Many machines (cluster) |
| Auto-scaling | ❌ Manual scale | ✅ HPA auto-scale |
| Self-healing | ⚠️ restart policy | ✅ Full self-healing |
| Rolling update | ⚠️ Basic | ✅ Zero-downtime |
| Complexity | Simple (1 YAML file) | Complex (many manifests) |
| Learning curve | 30 minutes | 30 days+ |
💡 The simple rule: one machine + a small team = Docker Compose | many machines + scale = Kubernetes
Summary
Docker Compose is a tool every developer should know — it reduces setting up a development environment to docker compose up, with no dependencies installed on the machine, everyone on the team on an identical environment, and "works on my machine" gone for good:
- Docker Compose = run many containers from one
docker-compose.ymlfile - The core commands =
up -d(run),down(stop),logs -f(read the logs),exec(get into a container) - .env files = keep secrets and config out of the YAML (do not commit them!)
- Multi-env = override files or profiles to separate dev from prod
- Volumes = named volumes for data, bind mounts for dev
- Networks = split the networks for security (frontend ≠ database)
- Health checks = take the pulse, plus
depends_on condition: service_healthy - Production = resource limits, logging, a read-only filesystem 🐳🐕