Docker Compose basisprincipes voor self-hosting beginners
Handleidingen 14 februari 2026 โ€ข 8 min read

Docker Compose basisprincipes voor self-hosting beginners

H

Hostly Team

Self-Hosting Enthusiast

Beheers Docker Compose in 20 minuten. Leer hoe je zelf gehoste applicaties kunt implementeren, beheren en problemen kunt oplossen met praktische voorbeelden die je echt zult gebruiken.

Every self-hosting guide says "just use Docker Compose" like it's the most obvious thing in the world. You copy a docker-compose.yml file, run docker compose up -d, and... pray it works. But what's actually happening? How do you fix things when they break? How do you customize configurations for your needs?

This guide demystifies Docker Compose for self-hosting beginners. No prior Docker knowledge required โ€” by the end, you'll understand what's in those YAML files and have the confidence to deploy, debug, and modify any self-hosted application.

What Is Docker and Why Should You Care?

Before Docker, installing software was a nightmare. Each application needed specific versions of dependencies, libraries, and runtimes โ€” and they'd often conflict with each other. Installing application A might break application B.

Docker solves this by packaging applications in containers โ€” self-contained environments with everything the application needs. Think of containers as lightweight virtual machines that:

  • Run in isolation โ€” apps can't interfere with each other
  • Are reproducible โ€” same container works identically everywhere
  • Start instantly โ€” no boot time like traditional VMs
  • Share the host's kernel โ€” extremely resource-efficient

Docker Compose extends this by letting you define and run multiple containers together. A typical self-hosted app might need:

  • The main application
  • A database (PostgreSQL, MySQL, MariaDB)
  • A cache (Redis, Memcached)
  • A reverse proxy (Caddy, Traefik, Nginx)

Docker Compose coordinates all of these with a single configuration file.

Installing Docker and Docker Compose

Let's get Docker installed. The official installation script works on most Linux distributions:

# Download and run the official install script
curl -fsSL https://get.docker.com | sh

# Add your user to the docker group (so you don't need sudo)
sudo usermod -aG docker $USER

# Log out and back in for group changes to take effect
exit

# After logging back in, verify installation
docker --version
# Docker version 25.x.x

docker compose version
# Docker Compose version v2.x.x

๐Ÿ“ Docker Compose v1 vs v2

The old docker-compose (with hyphen) is deprecated. Modern Docker uses docker compose (without hyphen) as a built-in plugin. All commands in this guide use the v2 syntax.

Anatomy of a docker-compose.yml File

Let's decode a typical Docker Compose file piece by piece. Here's a simple example that runs a web app with a database:

services:
  webapp:
    image: nginx:latest
    container_name: my-webapp
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
    environment:
      - TZ=America/New_York
    depends_on:
      - database
    networks:
      - app-network

  database:
    image: postgres:15
    container_name: my-database
    restart: unless-stopped
    environment:
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: secretpassword
      POSTGRES_DB: myapp_db
    volumes:
      - db-data:/var/lib/postgresql/data
    networks:
      - app-network

volumes:
  db-data:

networks:
  app-network:
    driver: bridge

Let's break down each section:

services:

The heart of the file. Each service becomes a container. Here we have two: webapp and database.

image:

Specifies which Docker image to use. Format is name:tag. Common tags:

  • latest โ€” most recent version (can change unexpectedly)
  • stable โ€” latest stable release
  • 15 or v2.1.0 โ€” specific version (recommended for production)

container_name:

Optional. Gives your container a specific name instead of an auto-generated one. Makes logs and debugging easier.

restart:

Controls auto-restart behavior:

  • no โ€” never restart (default)
  • always โ€” always restart, even after reboot
  • unless-stopped โ€” restart unless you manually stop it (recommended)
  • on-failure โ€” only restart if it crashes

ports:

Maps ports between host and container. Format: "HOST:CONTAINER"

ports:
  - "8080:80"     # Host port 8080 โ†’ Container port 80
  - "127.0.0.1:8080:80"  # Only accessible from localhost

volumes:

Persists data outside the container. Two types:

volumes:
  # Bind mount: maps a host directory
  - ./html:/usr/share/nginx/html:ro   # :ro = read-only
  
  # Named volume: managed by Docker
  - db-data:/var/lib/postgresql/data

Named volumes survive container destruction. Bind mounts let you edit files directly.

environment:

Sets environment variables inside the container. Two syntax options:

environment:
  - VAR_NAME=value           # List format
  TZ: America/New_York       # Map format

depends_on:

Ensures containers start in order. The webapp won't start until database is running.

networks:

Puts containers on the same virtual network so they can communicate using container names as hostnames.

Essential Docker Compose Commands

Here are the commands you'll use daily:

# Start all services (detached mode)
docker compose up -d

# Stop all services
docker compose down

# Stop and remove volumes too (careful - deletes data!)
docker compose down -v

# View running containers
docker compose ps

# View logs
docker compose logs              # All services
docker compose logs webapp       # Specific service
docker compose logs -f           # Follow (stream) logs
docker compose logs --tail=100   # Last 100 lines

# Restart services
docker compose restart           # Restart all
docker compose restart webapp    # Restart specific service

# Rebuild and restart (after changing docker-compose.yml)
docker compose up -d --force-recreate

# Pull latest images
docker compose pull

# Execute command inside container
docker exec -it my-webapp bash   # Open shell
docker exec my-webapp ls /app    # Run single command

Practical Example: Deploy Uptime Kuma

Let's deploy a real application: Uptime Kuma, a self-hosted monitoring tool. This example demonstrates a complete, working setup.

# Create a directory
mkdir ~/uptime-kuma
cd ~/uptime-kuma

# Create docker-compose.yml
nano docker-compose.yml

Paste this configuration:

services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    container_name: uptime-kuma
    restart: unless-stopped
    ports:
      - "3001:3001"
    volumes:
      - ./data:/app/data
    environment:
      - TZ=Europe/Paris

Launch it:

docker compose up -d

# Check it's running
docker compose ps

# View logs
docker compose logs -f

Visit http://your-server-ip:3001 โ€” you have a working monitoring tool!

Working with Environment Variables

Hardcoding passwords in docker-compose.yml is bad practice. Use environment files instead:

# Create .env file (same directory as docker-compose.yml)
nano .env
DB_PASSWORD=super-secret-password
DB_USER=myapp
[email protected]

Reference these in your compose file:

services:
  database:
    image: postgres:15
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}

Docker Compose automatically loads .env from the same directory.

๐Ÿ”’ Security Tip

Add .env to .gitignore so you don't accidentally commit secrets to version control.

Networking Explained

Understanding Docker networking prevents 90% of "why can't my app connect to the database?" problems.

Default Behavior

Without explicit network configuration, Docker Compose creates a default network. All services can reach each other using their service name as hostname:

# webapp can connect to database using:
postgres://database:5432/myapp_db
#         ^^^^^^^^ service name, not localhost!

Exposing Ports

Ports make services accessible from outside Docker:

ports:
  - "8080:80"           # Accessible from anywhere
  - "127.0.0.1:8080:80" # Only from host machine

If you're using a reverse proxy (Caddy, Traefik), you often don't need to expose ports at all โ€” the proxy handles external traffic.

Custom Networks

For complex setups, create explicit networks:

services:
  frontend:
    networks:
      - frontend-net
      
  backend:
    networks:
      - frontend-net
      - backend-net
      
  database:
    networks:
      - backend-net  # Not accessible from frontend!

networks:
  frontend-net:
  backend-net:

Managing Data with Volumes

Containers are ephemeral โ€” when they're removed, their internal data disappears. Volumes persist data:

Named Volumes (Recommended for databases)

services:
  database:
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:  # Docker manages this

View and manage volumes:

docker volume ls                    # List all volumes
docker volume inspect project_db-data  # Details
docker volume rm project_db-data       # Delete (careful!)

Bind Mounts (Good for config files)

volumes:
  - ./config:/app/config     # Relative path
  - /opt/data:/app/data      # Absolute path
  - ./config.yml:/app/config.yml:ro  # Single file, read-only

Debugging Common Issues

Container Won't Start

# Check container status
docker compose ps

# View detailed logs
docker compose logs service-name

# Check for port conflicts
sudo lsof -i :8080

Container Keeps Restarting

# View exit code and recent logs
docker compose ps
docker compose logs --tail=50 service-name

# Common causes:
# - Missing environment variables
# - Database connection failures (check depends_on)
# - Permission issues on volumes

"Cannot connect to database"

  • Use the service name, not localhost: database:5432
  • Check if database is actually ready (not just started)
  • Verify network configuration

Permission Denied on Volumes

# Check the container's user
docker exec service-name id

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

Adding a Reverse Proxy

A reverse proxy lets you access multiple services through one domain with HTTPS. Caddy is the easiest option:

services:
  caddy:
    image: caddy:latest
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    networks:
      - proxy-net

  # Your apps connect to the same network
  webapp:
    image: your-app
    networks:
      - proxy-net
    # No ports needed! Caddy handles external access

volumes:
  caddy-data:
  caddy-config:

networks:
  proxy-net:

Caddyfile:

app.yourdomain.com {
    reverse_proxy webapp:8080
}

monitoring.yourdomain.com {
    reverse_proxy uptime-kuma:3001
}

Updating Applications

Keep your self-hosted apps updated for security:

# Pull latest images
docker compose pull

# Recreate containers with new images
docker compose up -d

# Remove old unused images
docker image prune -f

For major version upgrades, always check the application's release notes for breaking changes or required migrations.

Backup Strategy

Back up your data regularly:

#!/bin/bash
# backup.sh - Run this as a cron job

BACKUP_DIR="/backup/$(date +%Y-%m-%d)"
mkdir -p "$BACKUP_DIR"

# Stop containers for consistent backup
cd /home/user/my-app
docker compose stop

# Backup bind mounts
tar -czf "$BACKUP_DIR/data.tar.gz" ./data ./config

# Backup named volumes
docker run --rm -v my-app_db-data:/source -v "$BACKUP_DIR":/backup alpine tar -czf /backup/db-data.tar.gz -C /source .

# Restart
docker compose up -d

Useful Tips and Tricks

Override for Development

Create docker-compose.override.yml for local changes that shouldn't be committed:

# docker-compose.override.yml
services:
  webapp:
    ports:
      - "8080:80"  # Expose directly for testing

Check Configuration Without Starting

docker compose config  # Validates and shows merged config

Run One-Off Commands

docker compose run --rm service-name command
# Example: run database migrations
docker compose run --rm webapp python manage.py migrate

Watch Resource Usage

docker stats  # Live resource monitoring

Quick Reference Card

TaskCommand
Start servicesdocker compose up -d
Stop servicesdocker compose down
View logsdocker compose logs -f
List containersdocker compose ps
Restart servicedocker compose restart name
Update imagesdocker compose pull && docker compose up -d
Shell into containerdocker exec -it name bash
View configdocker compose config

Next Steps

Now that you understand Docker Compose, you're ready to self-host anything. Here are some great starting points:

The self-hosting community is incredibly welcoming. When you get stuck, r/selfhosted is full of helpful people who've faced the same challenges.

You now have the foundation to deploy, debug, and maintain any self-hosted application. Welcome to the world of digital independence.