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

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