Reading time: ~18 minutes Audience: Homelab and self-hosting enthusiasts
What Is Docker Compose?
Overview
Docker Compose is a declarative tool for defining and running multi-container Docker applications. Instead of typing long docker run commands for each container, you write a YAML file that describes your entire application stack — services, networks, volumes, and environment variables. A single command (docker compose up -d) creates everything. This makes your infrastructure reproducible, version-controlled, and easy to share.
Why It Matters for Homelabs
Most homelab applications require multiple containers: a web app, a database, a cache, and a reverse proxy. Managing these with raw Docker commands is error-prone. Compose turns this into a single file that you can store in Git, deploy on any machine, and update with confidence.
Why Learn Docker Compose?
Declarative Infrastructure
Your entire stack is defined in code. If your server dies, you can clone the repo, run docker compose up, and be back online in minutes. This is the foundation of modern infrastructure as code.
Easy Updates and Rollbacks
Update a service by changing the image tag in the Compose file and running docker compose up -d. If something breaks, revert the tag and redeploy. The old container is recreated automatically.
Service Discovery and Networking
Compose automatically creates a private network for your stack. Containers can reach each other by service name (e.g., http://db:5432). No manual IP management or port mapping required for internal communication.
Prerequisites
Hardware Requirements
- Any Linux machine (mini PC, old laptop, VPS)
- 2 GB RAM minimum, 4 GB recommended
- 20 GB storage
Software Requirements
- Docker Engine 20.10+
- Docker Compose plugin v2.x
- A text editor (nano, vim, or VS Code)
Knowledge Prerequisites
- Basic Linux command line
- Understanding of Docker images and containers
- Familiarity with YAML indentation (spaces, not tabs)
Step 1: Install Docker Compose
Objective
Ensure the Docker Compose plugin is installed and working.
Step-by-Step Instructions
# Verify Docker Compose is installed
docker compose version
# Expected: Docker Compose version v2.x.x
# If missing, install it (Ubuntu/Debian)
sudo apt update
sudo apt install -y docker-compose-plugin
# Verify again
docker compose version
If you still have the legacy docker-compose (Python) binary, migrate to the plugin. The plugin is faster and supports the latest features.
Step 2: Write Your First Compose File
Objective
Create a simple stack with an Nginx web server.
Step-by-Step Instructions
Create a project directory:
mkdir -p ~/homelab-tutorial && cd ~/homelab-tutorial
Create docker-compose.yml:
version: "3.8"
services:
web:
image: nginx:alpine
container_name: my-nginx
restart: always
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
Create a simple HTML file:
mkdir -p html
echo "<h1>Hello from Docker Compose!</h1>" > html/index.html
Deploy:
docker compose up -d
Verify:
curl http://localhost:8080
# Output: <h1>Hello from Docker Compose!</h1>
Step 3: Add a Database and Connect Services
Objective
Extend the stack with a MariaDB database and demonstrate service discovery.
Step-by-Step Instructions
Update docker-compose.yml:
version: "3.8"
services:
web:
image: nginx:alpine
container_name: my-nginx
restart: always
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
depends_on:
- db
db:
image: mariadb:10.6
container_name: my-mariadb
restart: always
environment:
- MYSQL_ROOT_PASSWORD=secret_root_pass
- MYSQL_DATABASE=myapp
- MYSQL_USER=myapp
- MYSQL_PASSWORD=secret_app_pass
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:
Key observations:
- The web service can reach the database at db:3306 (service name becomes hostname) - depends_on ensures db starts before web, but does not wait for it to be ready - db_data is a named volume managed by Docker; data persists across container recreations
Deploy the updated stack:
docker compose up -d
Step 4: Use Environment Variables and Secrets
Objective
Externalize configuration with a .env file.
Step-by-Step Instructions
Create .env:
MYSQL_ROOT_PASSWORD=super_secret_root
MYSQL_DATABASE=myapp
MYSQL_USER=myapp
MYSQL_PASSWORD=super_secret_app
NGINX_PORT=8080
Update docker-compose.yml:
version: "3.8"
services:
web:
image: nginx:alpine
container_name: my-nginx
restart: always
ports:
- "${NGINX_PORT}:80"
volumes:
- ./html:/usr/share/nginx/html:ro
depends_on:
- db
db:
image: mariadb:10.6
container_name: my-mariadb
restart: always
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:
Add .env to .gitignore:
echo ".env" >> .gitignore
Step 5: Add a Reverse Proxy and Custom Network
Objective
Deploy Traefik as a reverse proxy with automatic HTTPS and a custom bridge network.
Step-by-Step Instructions
version: "3.8"
networks:
frontend:
driver: bridge
backend:
driver: bridge
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "[email protected]"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
networks:
- frontend
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.local`)"
- "traefik.http.routers.traefik.service=api@internal"
web:
image: nginx:alpine
container_name: my-nginx
restart: always
volumes:
- ./html:/usr/share/nginx/html:ro
networks:
- frontend
- backend
labels:
- "traefik.enable=true"
- "traefik.http.routers.web.rule=Host(`web.local`)"
- "traefik.http.routers.web.tls.certresolver=letsencrypt"
db:
image: mariadb:10.6
container_name: my-mariadb
restart: always
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
volumes:
- db_data:/var/lib/mysql
networks:
- backend
volumes:
db_data:
Pro Tips
Tip 1: Use Profiles for Conditional Services
services:
dev-tools:
image: phpmyadmin
profiles:
- dev
Deploy with docker compose --profile dev up -d to include dev tools only when needed.
Tip 2: Health Checks for Reliability
services:
db:
image: mariadb:10.6
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
Tip 3: Resource Limits
services:
web:
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
Tip 4: Override Files for Environments
Create docker-compose.override.yml for local tweaks (not committed to production):
services:
web:
ports:
- "8080:80"
Troubleshooting Common Issues
Problem 1: “port is already allocated”
# Find the conflicting process
sudo ss -tlnp | grep :80
# Kill it or change the host port in Compose
Problem 2: “Container unhealthy”
# Check the healthcheck definition
docker inspect <container> | jq '.[0].State.Health'
# Review logs
docker compose logs <service>
Problem 3: “Permission denied on volume”
# Check the user running inside the container
docker exec <container> id
# Fix host permissions
sudo chown -R 1000:1000 ./data
Conclusion
Summary
Docker Compose is the essential tool for homelab deployment. It transforms complex multi-service applications into readable, version-controlled YAML files. With this tutorial, you learned how to define services, connect them via networks, persist data with volumes, externalize secrets with .env, and front everything with a reverse proxy.
Next Steps
- Version-control your Compose files in Git
- Explore Docker Swarm for multi-node orchestration
- Add health checks and restart policies to all services
- Deploy a full homelab stack (Nextcloud, Pi-hole, monitoring)
Affiliate Opportunities
- prerequisites: hosting — VPS providers for remote Compose stacks
- step-1: tool — Docker Hub subscriptions, JetBrains IDEs
- pro-tips: service — Cloudflare for DNS and TLS
Internal Linking Strategy
what-is→ related_comparison: Docker Compose vs Podmanprerequisites→ setup_guide: Home server Docker setupconclusion→ next_steps: Docker Compose YAML examples
CTA
- [comment] What was your first Docker Compose stack? Share your beginner story.
- [newsletter] Subscribe for weekly Docker and homelab tutorials.
- [internal_link] Next: explore production-ready Compose examples