Table of Contents
- What is Docker Compose and Why Use It?
- Install Docker Engine and Docker Compose on Ubuntu
- docker-compose.yml File Structure
- Example: WordPress + MySQL + phpMyAdmin
- Example: Node.js + PostgreSQL + Redis
- Essential Docker Compose Commands
- Networks and Volumes
- Managing Secrets with .env Files
- Production-ready Tips
- Troubleshooting Common Issues
- Frequently Asked Questions
What is Docker Compose and Why Use It?
Modern applications rarely run in a single container. A typical e-commerce system needs a web server, database, cache, queue worker, and reverse proxy all working together. Docker Compose lets you define and manage all these multi-container applications through a single YAML file.
| Aspect | Docker (docker run) | Docker Compose |
|---|---|---|
| Containers | One at a time | Multiple services at once |
| Networking | Manual setup | Automatic per-project network |
| Volumes | Specified as flags | Declared in YAML |
| Dependencies | Manual management | depends_on + healthcheck |
| Restart policy | Per-container flag | restart: key in YAML |
Docker Compose is ideal for development environments and small-to-medium production deployments on a single VPS. For multi-server scaling, consider Docker Swarm or Kubernetes.
Install Docker Engine and Docker Compose on Ubuntu
Recommended: Ubuntu 22.04 LTS or 24.04 LTS for production.
Step 1 — Update packages and install prerequisites
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg lsb-release
Step 2 — Add Docker's GPG key and repository
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Step 3 — Install Docker Engine and Compose Plugin
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
Step 4 — Run Docker without sudo
sudo usermod -aG docker $USER
newgrp docker # or log out and back in
Verify Installation
docker --version # Docker version 26.x.x
docker compose version # Docker Compose version v2.x.x
docker compose (no hyphen) instead of the older docker-compose command. This guide uses v2 syntax throughout.
docker-compose.yml File Structure
services: # list of services/containers
web: # service name (also its hostname on the network)
image: nginx:alpine
ports:
- "80:80" # host_port:container_port
volumes:
- ./html:/usr/share/nginx/html:ro
depends_on:
- app
restart: unless-stopped
app:
build: . # build from Dockerfile in current directory
environment:
- NODE_ENV=production
env_file:
- .env
restart: unless-stopped
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
restart: unless-stopped
volumes: # named volumes (persistent)
pgdata:
Example: WordPress + MySQL + phpMyAdmin
mkdir ~/wordpress-stack && cd ~/wordpress-stack
# docker-compose.yml
services:
wordpress:
image: wordpress:6.7-php8.2-apache
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: mysql
WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
WORDPRESS_DB_USER: ${MYSQL_USER}
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- wp_data:/var/www/html
depends_on:
mysql:
condition: service_healthy
restart: unless-stopped
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
phpmyadmin:
image: phpmyadmin:5-apache
ports:
- "8081:80"
environment:
PMA_HOST: mysql
depends_on:
- mysql
restart: unless-stopped
volumes:
wp_data:
mysql_data:
# .env (never commit this to Git)
MYSQL_ROOT_PASSWORD=StrongRootPass2026!
MYSQL_DATABASE=wordpress_db
MYSQL_USER=wpuser
MYSQL_PASSWORD=StrongUserPass2026!
docker compose up -d
docker compose ps
Example: Node.js + PostgreSQL + Redis
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
REDIS_URL: redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --save 20 1 --loglevel warning
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
pg_data:
redis_data:
Essential Docker Compose Commands
| Command | Description |
|---|---|
docker compose up -d | Start all services in detached mode |
docker compose down | Stop and remove containers (volumes preserved) |
docker compose down -v | Stop and remove containers + volumes |
docker compose ps | Show status of all services |
docker compose logs -f | Stream logs from all services |
docker compose logs -f web | Stream logs from "web" service only |
docker compose exec app sh | Open shell in "app" container |
docker compose restart | Restart all services |
docker compose pull | Pull latest images |
docker compose build | Rebuild images from Dockerfile |
docker compose config | Validate and view the merged compose config |
Networks and Volumes
Networking
Docker Compose automatically creates a default network for your project. All services in the same file can reach each other using the service name as hostname:
DATABASE_URL=postgresql://user:pass@postgres:5432/mydb
# ^^^^^^^ service name
Volumes
- Named volumes (
pg_data:/var/lib/postgresql/data) — Docker-managed, persistent, recommended for databases - Bind mounts (
./src:/app/src) — Maps host directory into container, great for development
Managing Secrets with .env Files
Docker Compose automatically loads a .env file from the same directory. Use ${VARIABLE_NAME} in your compose file to reference variables.
# .gitignore — never commit .env!
.env
*.env
!.env.example
.env.example file with variable names but no real values so teammates know what to configure without exposing secrets.
Production-ready Tips
1. Restart Policy
restart: unless-stopped # always restart, except manual docker compose stop
2. Resource Limits
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
memory: 256M
3. Healthchecks
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
4. Reverse Proxy with Nginx
Run Nginx as a separate service (or outside Docker Compose) acting as a reverse proxy and handling SSL termination via Let's Encrypt/Certbot.
5. Volume Backups
docker run --rm \
-v wordpress-stack_mysql_data:/data \
-v $(pwd)/backup:/backup \
alpine tar czf /backup/mysql-$(date +%Y%m%d).tar.gz /data
Troubleshooting Common Issues
| Problem | Cause | Solution |
|---|---|---|
| Container exits immediately | Entrypoint error or healthcheck fail | Check with docker compose logs <service> |
| Port already in use | Port conflicts with another process | Run sudo ss -tlnp | grep PORT to identify |
| Volume data lost | Ran down -v unintentionally | Verify backups before running down |
| Services can't communicate | Services on different networks | Check with docker compose config |
| Permission denied on volume | UID/GID mismatch between host and container | Add user: "UID:GID" to the service |
Frequently Asked Questions
restart: unless-stopped to every service in docker-compose.yml. Docker Engine will automatically restart containers when the system boots up.docker compose pull to fetch latest images, then docker compose up -d to recreate containers with the new images. Services without changes won't be restarted.Summary
Docker Compose gives you an organized way to manage multi-container applications on a single VPS. The key steps: install Docker Engine + Compose Plugin, write a docker-compose.yml defining your services, store secrets in .env files, and add restart policies for automatic recovery after reboots.
For Docker Compose in production, we recommend a VPS with at least 2GB RAM and SSD storage. AsiaGB offers Thailand and Singapore VPS with SSD, 99% uptime guarantee, and competitive pricing.