Docker Compose for Self-Hosted Services: From Zero to Production

Prerequisites

  • A Linux server (Ubuntu/Debian recommended) with a non-root user with sudo access
  • Docker and Docker Compose installed (sudo apt install docker.io docker-compose-plugin)
  • A domain name pointing to your server (or you can use localhost for testing)
  • Basic familiarity with the command line and text editors

Why Docker Compose for Self-Hosting?

Running multiple services on a single server is a common homelab scenario. Docker Compose lets you define your entire stack in one file and bring everything up with a single command. No more systemd service files for each app, no hunting down which port conflicts with which service.

Step 1: Create Your Project Structure

Start by creating a dedicated directory for your services:

mkdir -p ~/docker-stack/{traefik,www,apps}
cd ~/docker-stack
touch docker-compose.yml
chmod 644 docker-compose.yml

Step 2: Define Your Reverse Proxy

We’ll use Traefik as a lightweight reverse proxy. It integrates beautifully with Docker Compose and handles Let’s Encrypt automatically.

version: '3.8'

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.myresolver.acme.httpchallenge=true"
      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
      - "[email protected]"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
      - "--api.insecure=true"
      - "--log.level=INFO"
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./traefik/letsencrypt:/letsencrypt"
    networks:
      - proxy
    restart: unless-stopped

networks:
  proxy:
    driver: bridge

Step 3: Add Your First Service (Pi-hole)

Each service gets labels for Traefik routing. Here’s Pi-hole with automatic HTTPS:

  pihole:
    image: pihole/pihole:latest
    container_name: pihole
    environment:
      TZ: "America/New_York"
      WEBPASSWORD: "change_this_password"
    volumes:
      - "./apps/pihole/etc-pihole:/etc/pihole"
      - "./apps/pihole/etc-dnsmasq.d:/etc/dnsmasq.d"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.pihole.rule=Host(`pihole.yourdomain.com`)"
      - "traefik.http.routers.pihole.entrypoints=websecure"
      - "traefik.http.routers.pihole.tls.certresolver=myresolver"
      - "traefik.http.services.pihole.loadbalancer.server.port=80"
    networks:
      - proxy
      - default
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "67:67/udp"
    cap_add:
      - NET_ADMIN
    restart: unless-stopped

Step 4: Initialize and Launch

Create the Let’s Encrypt storage and start your stack:

mkdir -p traefik/letsencrypt
touch traefik/letsencrypt/acme.json
chmod 600 traefik/letsencrypt/acme.json
docker compose up -d

Common Pitfalls & Troubleshooting

  • Port 53 conflicts: Stop systemd-resolved or change Pi-hole’s port.
  • Let’s Encrypt rate limits: Test on http://server-ip:8080 first.
  • Permission denied on acme.json: Must be chmod 600 or Traefik won’t start.
  • DNS not resolving: Verify A record points to your server’s public IP.

Going Further

Once you’re comfortable, try using .env files for sensitive data and adding healthcheck labels for automatic restarts. Docker Compose transforms self-hosting from “random commands” into “documented reproducible infrastructure.”

Leave a Reply

Your email address will not be published. Required fields are marked *