blog_tech/i18n/en/docusaurus-plugin-content-docs/current/homelab-actuel/docker-compose.md

17 KiB

sidebar_position tags last_update
3
docker
docker-compose
containerization
homelab
date
2025-11-25

Docker and Docker Compose

What is Docker?

Docker is a containerization platform that allows you to package applications and their dependencies into lightweight and isolated containers.

Containers: revolution of modern infrastructure

A container is a standardized software unit that contains:

  • The application itself
  • All its dependencies (libraries, runtime, system tools)
  • An isolated filesystem
  • Environment variables and configuration

Difference with virtual machines:

  • Container: Shares the host OS kernel, starts in seconds, very lightweight (~MB)
  • VM: Emulates a complete OS, starts in minutes, heavier (~GB)

Advantages of Docker

  1. Portability: "Runs anywhere" - works identically in development, testing, and production
  2. Isolation: Each container is isolated, avoiding dependency conflicts
  3. Lightweight: Consumes fewer resources than a VM (no full virtualization)
  4. Speed: Instant application startup
  5. Reproducibility: Docker image = identical environment every time
  6. Ecosystem: Docker Hub contains thousands of ready-to-use images

Docker Compose: simplified orchestration

Docker Compose is an orchestration tool for defining and managing multi-container applications.

Why Docker Compose?

Without Compose, deploying an application with multiple containers (app + database + cache + ...) requires long docker run commands that are difficult to maintain.

With Compose:

  • Declarative configuration: Everything is defined in a compose.yml file
  • Grouped management: Start/stop all services with one command
  • Automatic networks: Containers communicate easily with each other
  • Persistent volumes: Simple storage management
  • Environment variables: Flexible configuration via .env files

compose.yml file

A Compose file defines:

  • Services (containers)
  • Networks (communication between containers)
  • Volumes (data persistence)
  • Environment variables (configuration)

Docker Compose stack examples

My Docker Compose stacks are available in the Ansible repository under stacks/. Here are some representative examples:

Example 1: Traefik - Advanced Reverse Proxy

Traefik is the entry point for the entire infrastructure. This compose illustrates an advanced configuration with two Traefik instances (public and private):

services:
  traefik-public:
    image: traefik:v3
    container_name: traefik-public
    restart: unless-stopped
    ports:
      - "192.168.1.2:80:80"
      - "192.168.1.2:443:443"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    networks:
      - traefik_network
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik-public.yml:/etc/traefik/traefik.yml:ro
      - ./dynamic-public:/etc/traefik/dynamic:ro
      - ./letsencrypt-public:/letsencrypt
      - /var/log/traefik:/var/log/traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik-dashboard-public.rule=Host(`traefik-public.local.tellserv.fr`)"
      - "traefik.http.routers.traefik-dashboard-public.entrypoints=local"
      - "traefik.http.routers.traefik-dashboard-public.tls.certresolver=cloudflare-local"
      - "traefik.http.routers.traefik-dashboard-public.tls=true"
      - "traefik.http.routers.traefik-dashboard-public.service=api@internal"
      - "traefik.http.middlewares.crowdsec-bouncer.forwardauth.address=http://crowdsec-bouncer:8080/api/v1/forwardAuth"
    environment:
      - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
      - TZ=Europe/Paris

  traefik-private:
    image: traefik:v3
    container_name: traefik-private
    restart: unless-stopped
    ports:
      - "192.168.1.3:80:80"
      - "192.168.1.3:443:443"
    networks:
      - traefik_network
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik-private.yml:/etc/traefik/traefik.yml:ro
      - ./dynamic-private:/etc/traefik/dynamic:ro
      - ./letsencrypt-private:/letsencrypt
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik-dashboard-local.rule=Host(`traefik-private.local.tellserv.fr`)"
    environment:
      - TZ=Europe/Paris
      - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}

networks:
  traefik_network:
    external: true

Key points:

  • Two instances: Separation of public (Internet) and private (local network only)
  • Docker socket: Traefik automatically detects new containers via /var/run/docker.sock
  • Let's Encrypt certificates: Automatic generation with DNS-01 challenge (Cloudflare)
  • Traefik labels: Dynamic configuration via Docker labels
  • CrowdSec middleware: Integration with CrowdSec to block malicious IPs
  • External network: All services connect to the traefik_network network

Example 2: Photoprism - Application with database

Photoprism illustrates a classic application stack (app + DB) with advanced configuration:

services:
  photoprism:
    image: photoprism/photoprism:241021
    stop_grace_period: 10s
    depends_on:
      - mariadb
    restart: unless-stopped
    security_opt:
      - seccomp:unconfined
      - apparmor:unconfined
    working_dir: "/photoprism"
    volumes:
      - "/mnt/storage/photos:/photoprism/import"
      - "/mnt/storage/photoprism/originals:/photoprism/originals"
      - "/mnt/storage/photoprism/storage:/photoprism/storage"
    environment:
      - PHOTOPRISM_DATABASE_DRIVER=mysql
      - PHOTOPRISM_DATABASE_SERVER=mariadb:3306
      - PHOTOPRISM_DATABASE_NAME=photoprism
      - PHOTOPRISM_DATABASE_USER=${MARIADB_USER}
      - PHOTOPRISM_DATABASE_PASSWORD=${PHOTOPRISM_DATABASE_PASSWORD}
      - PHOTOPRISM_ADMIN_USER=${PHOTOPRISM_ADMIN_USER}
      - PHOTOPRISM_ADMIN_PASSWORD=${PHOTOPRISM_ADMIN_PASSWORD}
      - PHOTOPRISM_SITE_URL=https://photoprism.tellserv.fr/
      - PHOTOPRISM_HTTP_COMPRESSION=gzip
      - PHOTOPRISM_JPEG_QUALITY=85
    networks:
      - traefik_network
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-local.rule=Host(`${COMPOSE_PROJECT_NAME}.local.tellserv.fr`)"
      - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-local.entryPoints=local"
      - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-local.tls=true"
      - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-prod.rule=Host(`${COMPOSE_PROJECT_NAME}.tellserv.fr`)"
      - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-prod.entryPoints=websecure"
      - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-prod.tls.certResolver=cloudflare"
      - "traefik.http.services.${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=2342"
      - "com.centurylinklabs.watchtower.enable=true"

  mariadb:
    image: mariadb:11
    restart: unless-stopped
    stop_grace_period: 5s
    command: >
      --innodb-buffer-pool-size=512M
      --transaction-isolation=READ-COMMITTED
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_unicode_ci
      --max-connections=512
    volumes:
      - ./database:/var/lib/mysql
    environment:
      - MARIADB_DATABASE=photoprism
      - MARIADB_USER=${MARIADB_USER}
      - MARIADB_PASSWORD=${MARIADB_PASSWORD}
      - MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
    networks:
      - traefik_network

networks:
  traefik_network:
    external: true

Key points:

  • Dependencies: depends_on ensures MariaDB starts before Photoprism
  • Mounted volumes: Access to MergerFS storage (/mnt/storage) for photos
  • Database: MariaDB optimized for Photoprism (buffer pool, character set UTF-8)
  • Environment variables: Secrets injected via .env file (not versioned)
  • Dual exposure: Accessible locally (.local.tellserv.fr) and on the Internet (.tellserv.fr)
  • Watchtower: Label to enable automatic updates
  • DB optimizations: Adapted MariaDB configuration (buffer pool, connections, charset)

Example 3: Mobilizon - Multi-container application with internal network

Mobilizon demonstrates the use of multiple Docker networks (external + internal):

services:
  mobilizon:
    user: "1000:1000"
    restart: always
    image: docker.io/framasoft/mobilizon
    env_file: .env
    depends_on:
      - db
    volumes:
      - ./uploads:/var/lib/mobilizon/uploads
      - ./tzdata:/var/lib/mobilizon/tzdata
    networks:
      - traefik_network
      - mobilizon_internal
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.mobilizon-local.rule=Host(`mobilizon.local.tellserv.fr`)"
      - "traefik.http.routers.mobilizon-local.entryPoints=local"
      - "traefik.http.routers.mobilizon-prod.rule=Host(`mobilizon.tellserv.fr`)"
      - "traefik.http.routers.mobilizon-prod.entryPoints=websecure"
      - "traefik.http.routers.mobilizon-prod.tls.certResolver=cloudflare"
      - "traefik.http.services.mobilizon.loadbalancer.server.port=5005"

  db:
    image: docker.io/postgis/postgis:15-3.4
    restart: always
    env_file: .env
    volumes:
      - ./db:/var/lib/postgresql/data:z
    networks:
      - mobilizon_internal

networks:
  mobilizon_internal:
    ipam:
      driver: default
  traefik_network:
    external: true

Key points:

  • Two networks:
    • traefik_network (external): Mobilizon communicates with Traefik
    • mobilizon_internal (internal): Private communication between Mobilizon and PostgreSQL
  • Security: The database is not exposed on the Traefik network
  • PostgreSQL with PostGIS: Geographic extension to manage geolocated events
  • User ID: Execution with specific UID/GID to manage file permissions
  • Volume with SELinux: Flag :z for SELinux compatibility

Example 4: Vaultwarden - Secrets management

Vaultwarden (password manager) shows a security-focused configuration:

services:
  vaultwarden:
    image: vaultwarden/server:1.32.7
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      - TZ=Europe/Paris
      - ADMIN_TOKEN=${VAULTWARDEN_ADMIN_TOKEN}
      - SIGNUPS_ALLOWED=${SIGNUPS_ALLOWED}
      - SMTP_FROM=${SMTP_FROM}
      - SMTP_HOST=${SMTP_HOST}
      - SMTP_PORT=${SMTP_PORT}
      - SMTP_SECURITY=${SMTP_SECURITY}
      - SMTP_USERNAME=${SMTP_USERNAME}
      - SMTP_PASSWORD=${SMTP_PASSWORD}
      - EXPERIMENTAL_CLIENT_FEATURE_FLAGS=ssh-key-vault-item,ssh-agent
    volumes:
      - ./vw-data:/data
    networks:
      - traefik_network
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.vaultwarden-local.rule=Host(`vaultwarden.local.tellserv.fr`)"
      - "traefik.http.routers.vaultwarden-prod.rule=Host(`vaultwarden.tellserv.fr`)"
      - "traefik.http.routers.vaultwarden-prod.tls.certResolver=cloudflare"
      - "com.centurylinklabs.watchtower.enable=true"

networks:
  traefik_network:
    external: true

Key points:

  • Secrets via .env: All passwords and tokens in environment variables
  • SMTP configuration: Email sending for notifications and account recovery
  • Experimental features: SSH key support in the vault
  • Data volume: Vault persistence in ./vw-data
  • Secure exposure: HTTPS mandatory via Traefik with Let's Encrypt

Patterns and best practices

1. External network traefik_network

All my services use a shared external Docker network:

networks:
  traefik_network:
    external: true

Advantages:

  • Traefik automatically detects new services
  • Communication between services via their names (e.g. http://vaultwarden)
  • Isolation by default (unconnected services cannot communicate)

2. Traefik labels for dynamic configuration

Instead of static configuration files, I use Docker labels:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.myapp.rule=Host(`myapp.tellserv.fr`)"
  - "traefik.http.routers.myapp.tls.certResolver=cloudflare"

Advantages:

  • Configuration colocated with the service
  • Deploying a new service = automatic addition to Traefik
  • No manual Traefik reload

3. Environment variables with .env files

All secrets are extracted into .env files:

VAULTWARDEN_ADMIN_TOKEN=supersecret123
MARIADB_PASSWORD=dbpassword456
CF_DNS_API_TOKEN=cloudflare_token_789

Advantages:

  • No secrets in plain text in versioned Compose files
  • .env files generated dynamically by Ansible (Jinja2 templates)
  • Easy secret rotation

4. Dual exposure: local and production

Each service has two entries:

  • Local: service.local.tellserv.fr (local network only)
  • Production: service.tellserv.fr (accessible from the Internet)
labels:
  - "traefik.http.routers.myapp-local.rule=Host(`myapp.local.tellserv.fr`)"
  - "traefik.http.routers.myapp-local.entryPoints=local"
  - "traefik.http.routers.myapp-prod.rule=Host(`myapp.tellserv.fr`)"
  - "traefik.http.routers.myapp-prod.entryPoints=websecure"

Advantages:

  • Fast local access (no Internet latency)
  • Remote access possible when needed
  • Ability to restrict certain services to local only

5. Restart policies and graceful shutdown

Resilience configuration:

restart: unless-stopped
stop_grace_period: 10s
  • unless-stopped: Restarts automatically except if manually stopped
  • stop_grace_period: Time to terminate cleanly before SIGKILL

6. Watchtower for update monitoring

Label to enable monitoring:

labels:
  - "com.centurylinklabs.watchtower.enable=true"

Important: Watchtower is used only for notifications of new available image versions. Updates are performed manually to maintain control over changes.

In the Future Homelab, automated update management will be implemented via Renovate Bot integrated directly with Forgejo.

Managing stacks with Docker Compose

Essential commands

# Start all services
docker compose up -d

# Stop all services
docker compose down

# View logs for a service
docker compose logs -f service_name

# Restart a service
docker compose restart service_name

# Update images and redeploy
docker compose pull
docker compose up -d

# View container status
docker compose ps

Deployment via Ansible

In my configuration, stacks are automatically deployed by Ansible:

  1. Generation of .env files from templates
  2. Synchronization of stacks/ folders to /opt/stacks/
  3. Execution of docker compose up -d for each stack

See the Ansible Playbooks page for more details.

Advantages of Docker Compose for a homelab

Simplicity

  • Readable and maintainable YAML files
  • No complex syntax like Kubernetes
  • Gentle learning curve

Performance

  • Instant service startup
  • Low overhead (no Kubernetes cluster)
  • Ideal for modest machines

Flexibility

  • Easy to add/remove services
  • Ability to quickly test new applications
  • Configuration by environment (dev, staging, prod)

Rich ecosystem

  • Docker Hub: thousands of ready-to-use images
  • LinuxServer.io: optimized and well-maintained images
  • Active community: documentation and support

Docker Compose limitations

Despite its advantages, Docker Compose has limitations for large-scale production use:

  1. No high availability: Everything is on a single machine
  2. No horizontal scaling: Impossible to distribute load across multiple servers
  3. No advanced orchestration: No rolling updates, canary deployments, etc.
  4. Manual management: Deployments via Ansible, no native GitOps

Note: Using restart: unless-stopped ensures automatic restart of containers after an unexpected stop, providing a basic form of resilience.

These limitations explain why I'm migrating to Kubernetes (K3S) for the future homelab. See the Future Homelab section.

Why not Docker Swarm?

When considering the evolution of my infrastructure, Docker Swarm was evaluated as an alternative to Kubernetes for container orchestration.

Docker Swarm: a tempting but outdated choice

Advantages of Docker Swarm:

  • Natively integrated with Docker (no additional installation)
  • Simpler configuration than Kubernetes
  • Gentler learning curve
  • Uses Docker Compose files directly (with some adaptations)
  • Less resource-intensive than Kubernetes

Why I didn't choose it:

  1. Kubernetes is the industry standard: The vast majority of companies use Kubernetes in production. Learning K8S provides skills directly transferable to the professional world.

  2. Ecosystem and community: Kubernetes benefits from a much richer ecosystem (Helm, operators, numerous DevOps tools) and a much larger community.

  3. Advanced features: Kubernetes offers capabilities that Docker Swarm doesn't have:

    • More advanced rolling updates and rollbacks
    • Fine-grained resource management (CPU/RAM limits, requests)
    • More elaborate network policies
    • Native GitOps support (ArgoCD, Flux)
    • Better integrated distributed storage (CSI drivers)
  4. Evolution and support: Docker Inc. has clearly oriented its development toward Kubernetes rather than Swarm. Swarm is maintained, but no longer evolves much.

  5. Learning objective: My goal being to acquire modern DevOps skills, mastering Kubernetes is a better long-term investment.

Conclusion: Although Docker Swarm is simpler and sufficient for many homelabs, I preferred to invest directly in learning Kubernetes, which has become the essential standard for container orchestration.