Reading time: ~18 minutes Audience: Homelab and self-hosting enthusiasts


What Is Docker Compose?

Overview

Docker Compose is a tool for defining and running multi-container Docker applications. With a single YAML file, you configure services, networks, and volumes. One command (docker compose up -d) brings the entire stack online. For homelab operators, Compose is the standard deployment method because it is declarative, version-controlled, and reproducible across machines.

Key Concepts

  • Services: Individual containers (e.g., nginx, db, app)
  • Networks: Isolated communication channels between services
  • Volumes: Persistent storage for container data
  • Environment Variables: Secrets and configuration passed at runtime
  • Profiles: Conditional service groups (e.g., dev, prod)

Prerequisites

Hardware Requirements

  • Any Linux server with Docker and Docker Compose installed
  • At least 2 GB RAM for small stacks, 8 GB+ for full monitoring suites

Software Requirements

  • Docker Engine 20.10+
  • Docker Compose plugin (v2.x)
  • A text editor and basic YAML syntax knowledge

Knowledge Prerequisites

  • Understanding of Docker images, containers, and ports
  • Familiarity with reverse proxies and TLS
  • Basic Linux file permissions

Step 1: Basic Web Application Stack

Objective

Deploy a simple web application with a database and reverse proxy.

docker-compose.yml

version: "3.8"

services:
  db:
    image: mariadb:10.6
    container_name: app-db
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=***      - MYSQL_DATABASE=app
      - MYSQL_USER=app
      - MYSQL_PASSWORD=***    volumes:
      - db_data:/var/lib/mysql

  app:
    image: nginx:alpine
    container_name: app-web
    restart: always
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
    depends_on:
      - db

volumes:
  db_data:

Deploy

docker compose up -d
docker compose logs -f

Step 2: Traefik Reverse Proxy with Automatic HTTPS

Objective

Deploy Traefik as a central reverse proxy with Let’s Encrypt and Docker service discovery.

docker-compose.yml

version: "3.8"

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: always
    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"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.rule=Host(`traefik.yourdomain.com`)"
      - "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.routers.traefik.middlewares=auth"
      - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8C/"

  whoami:
    image: traefik/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.yourdomain.com`)"
      - "traefik.http.routers.whoami.tls.certresolver=letsencrypt"

Step 3: Monitoring Stack (Prometheus + Grafana)

Objective

Deploy a complete monitoring stack with metrics collection, storage, and visualization.

docker-compose.yml

version: "3.8"

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: always
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: always
    ports:
      - "3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=***
  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    restart: always
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--path.rootfs=/rootfs'

volumes:
  prometheus_data:
  grafana_data:

prometheus.yml

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']
  - job_name: 'node'
    static_configs:
      - targets: ['node-exporter:9100']

Step 4: Nextcloud with PostgreSQL and Redis

Objective

Deploy a production-ready Nextcloud stack with a high-performance database and memory caching.

docker-compose.yml

version: "3.8"

services:
  app:
    image: nextcloud:apache
    container_name: nextcloud
    restart: always
    ports:
      - "8080:80"
    volumes:
      - nextcloud:/var/www/html
    environment:
      - POSTGRES_HOST=db
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=nextcloud
      - POSTGRES_PASSWORD=***      - NEXTCLOUD_ADMIN_USER=admin
      - NEXTCLOUD_ADMIN_PASSWORD=***      - NEXTCLOUD_TRUSTED_DOMAINS=cloud.yourdomain.com
      - REDIS_HOST=redis
    depends_on:
      - db
      - redis

  db:
    image: postgres:15-alpine
    container_name: nextcloud-db
    restart: always
    volumes:
      - db:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=nextcloud
      - POSTGRES_PASSWORD=***
  redis:
    image: redis:alpine
    container_name: nextcloud-redis
    restart: always

  cron:
    image: nextcloud:apache
    restart: always
    volumes:
      - nextcloud:/var/www/html:ro
    entrypoint: /cron.sh
    depends_on:
      - db
      - redis

volumes:
  nextcloud:
  db:

Step 5: Pi-hole with Unbound

Objective

Deploy a network-wide ad blocker with a recursive DNS resolver for maximum privacy.

docker-compose.yml

version: "3.8"

services:
  pihole:
    image: pihole/pihole:latest
    container_name: pihole
    restart: always
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "80:80/tcp"
    environment:
      - TZ=Asia/Singapore
      - WEBPASSWORD=***      - FTLCONF_LOCAL_IPV4=192.168.1.10
    volumes:
      - ./etc-pihole:/etc/pihole
      - ./etc-dnsmasq.d:/etc/dnsmasq.d
    cap_add:
      - NET_ADMIN
    dns:
      - 127.0.0.1
      - 1.1.1.1

  unbound:
    image: mvance/unbound:latest
    container_name: unbound
    restart: always
    ports:
      - "5335:53/tcp"
      - "5335:53/udp"

In Pi-hole, set Custom DNS to 127.0.0.1#5335.


Pro Tips

Tip 1: Use .env Files for Secrets

Never commit secrets to your Compose file. Use a .env file:

DB_PASSWORD=supersecret
DOMAIN=yourdomain.com

And reference variables in Compose:

environment:
  - MYSQL_PASSWORD=${DB_PASSWORD}

Add .env to .gitignore.

Tip 2: Health Checks and Restart Policies

Add health checks to ensure services recover automatically:

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:80"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s

Tip 3: Resource Limits

Prevent a runaway container from consuming all RAM:

deploy:
  resources:
    limits:
      cpus: '1.0'
      memory: 512M
    reservations:
      cpus: '0.25'
      memory: 128M

Tip 4: Named Volumes vs Bind Mounts

  • Named volumes: Managed by Docker, portable, best for databases
  • Bind mounts: Direct host filesystem mapping, best for config files and media

Troubleshooting Common Issues

Problem 1: Port Already in Use

# Find the process using the port
sudo ss -tlnp | grep :80

# Kill or reconfigure the conflicting service

Problem 2: Permission Denied on Bind Mounts

# Fix ownership
sudo chown -R 1000:1000 ./data

# Or use Docker user namespaces

Problem 3: Container Exits Immediately

# Check logs
docker compose logs <service_name>

# Inspect the container
docker inspect <container_name>

Conclusion

Summary

Docker Compose is the backbone of modern homelab infrastructure. It turns complex multi-service applications into declarative, reproducible configurations. The examples in this guide cover the most common patterns: reverse proxying, monitoring, cloud storage, and DNS filtering. With these templates, you can deploy a full homelab stack in minutes.

Next Steps

  • Version-control your Compose files in Git
  • Add health checks and resource limits to all services
  • Configure automated backups for named volumes
  • Experiment with Docker Swarm for multi-node orchestration

Affiliate Opportunities

  • prerequisites: hosting — VPS for remote Compose stacks
  • step-1: tool — Docker Hub subscriptions, JetBrains IDEs
  • pro-tips: service — Cloudflare, S3-compatible storage

Internal Linking Strategy

CTA

  • [comment] Which Compose stack is your favorite? Share your custom configurations.
  • [newsletter] Get weekly Docker and homelab deployment guides.
  • [internal_link] Next: learn Docker Compose for beginners