Self-hosted • Privacy-first • No tracking
Home / Homelab / Docker Compose Best Practices for Homelab: A Complete Guide to Production-Grade Self-Hosting
Homelab #homelab#docker-compose#self-hosting#traefik#backups ⏱ 11 min • 👁 1 • Aug 30, 2026

Docker Compose Best Practices for Homelab: A Complete Guide to Production-Grade Self-Hosting

Master Docker Compose for homelab: step-by-step guide covering security, version pinning, backups, and troubleshooting common pitfalls.

AdSense — Top (970x90) • Responsive
Docker Compose Best Practices for Homelab: A Complete Guide to Production-Grade Self-Hosting

Introduction

Running a homelab is an exercise in controlled chaos. You start with a single container, then add a reverse proxy, a database, a monitoring stack, and before you know it, you have 15 services running from memory. The difference between a fragile setup and a resilient one often comes down to how you write your docker-compose.yml files. This guide is not about the basics—it's about the discipline that separates hobby projects from reliable infrastructure.

You will learn how to structure your Compose files for maintainability, pin versions correctly to avoid surprise breakage, handle secrets without leaking them into version control, and implement backup strategies that actually work. We will also cover advanced hardening techniques and a troubleshooting table for the most common failure modes. By the end, you will have a repeatable pattern for deploying any self-hosted application with confidence.

This guide assumes you are comfortable with the Linux command line and have basic Docker knowledge. We will focus on practical, copy-pasteable examples that you can adapt to your own stack immediately. All commands are written for Bash and are designed to be run sequentially.

Note on versions: The examples use the :latest tag where no specific version is verified. This is intentional—you should always check the official GitHub releases page before pinning a version. The version above may be outdated by now.

Prerequisites

Before you start, ensure your homelab host meets the following minimum requirements. These are typical values, not hard guarantees—your actual usage may vary depending on the number of containers and their workload.

Component Minimum Recommended Notes
CPU 2 cores 4+ cores More cores help with parallel builds and database queries.
RAM 4 GB 8 GB+ Each container typically uses 100-500 MB. Databases like PostgreSQL can use more.
Storage 20 GB free 100 GB+ SSD SSDs drastically improve database and indexing performance.
OS Ubuntu 22.04 LTS Debian 12 Any modern Linux distribution works. Ensure kernel supports cgroups v2.
Docker Engine 24.0+ 26.0+ Older versions lack some Compose features. Check with docker --version.
Docker Compose Plugin v2.20+ v2.27+ The standalone docker-compose binary is deprecated. Use the plugin.
Git 2.30+ Latest For versioning your Compose files.

Software to install:

sudo apt update && sudo apt upgrade -y && sudo apt install -y git curl nano

Install Docker Engine and Compose plugin:

curl -fsSL https://get.docker.com | sh && sudo usermod -aG docker $USER && newgrp docker

Verify the installation:

docker --version && docker compose version

Step-by-Step Installation Guide

Step 1: Create a Project Directory and .env File

Every service gets its own directory. This keeps configurations isolated and makes backups trivial. Start by creating a directory for your first service—we'll use paperless-ngx as an example.

mkdir -p ~/paperless-ngx && cd ~/paperless-ngx

Now create a .env file to store all secrets and configurable variables. Never commit this file to Git. It contains sensitive data.

nano .env

Add the following content, replacing the placeholder values with your own:

POSTGRES_PASSWORD=change_this_strong_password
PAPERLESS_SECRET_KEY=generate_a_random_64_char_string
PAPERLESS_TIME_ZONE=America/New_York
PAPERLESS_URL=https://docs.example.com

Generate a strong secret key with:

openssl rand -hex 32

Step 2: Write the Docker Compose File

Create a docker-compose.yml file in the same directory. This example includes PostgreSQL as the database and Redis for caching.

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U paperless"]
      interval: 10s
      timeout: 5s
      retries: 5

  broker:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redisdata:/data

  app:
    image: paperless-ngx:latest
    restart: unless-stopped
    ports:
      - "8000:8000"
    volumes:
      - ./data:/usr/src/paperless/data
      - ./media:/usr/src/paperless/media
      - ./export:/usr/src/paperless/export
      - ./consume:/usr/src/paperless/consume
    env_file:
      - .env
    environment:
      PAPERLESS_REDIS: redis://broker:6379
      PAPERLESS_DBHOST: db
    depends_on:
      db:
        condition: service_healthy
      broker:
        condition: service_started

volumes:
  pgdata:
  redisdata:

Check the official GitHub releases page before pinning a version — the version above may be outdated by now.

Step 3: Pull Images and Start Services

Pull all images first to catch any authentication issues early.

docker compose pull

Then start the services in detached mode.

docker compose up -d

Check the status of all containers:

docker compose ps

Step 4: Set Up the Reverse Proxy (Traefik)

A reverse proxy routes traffic to your services based on domain names. Traefik integrates natively with Docker and automatically discovers new containers via labels. Create a separate directory for Traefik.

mkdir -p ~/traefik && cd ~/traefik

Create a docker-compose.yml for Traefik:

services:
  traefik:
    image: traefik:v3.1
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
      - ./config:/etc/traefik
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"

Now add Traefik labels to your Paperless service. Edit ~/paperless-ngx/docker-compose.yml and add the following under the app service:

    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.paperless.rule=Host(`docs.example.com`)"
      - "traefik.http.routers.paperless.entrypoints=websecure"
      - "traefik.http.routers.paperless.tls.certresolver=letsencrypt"

Step 5: Configure Automatic Backups

Backups are non-negotiable. Use a separate container to back up your PostgreSQL database and files. Add a backup service to your Paperless Compose file:

  backup:
    image: prodrigestivill/postgres-backup-local:16
    restart: unless-stopped
    volumes:
      - ./backups:/backups
    environment:
      POSTGRES_HOST: db
      POSTGRES_DB: paperless
      POSTGRES_USER: paperless
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      SCHEDULE: "@daily"
      BACKUP_KEEP_DAYS: 7
    depends_on:
      db:
        condition: service_healthy

For file backups, use a simple cron job on the host:

crontab -e

Add the following line to back up the data directory daily at 2 AM:

0 2 * * * tar -czf ~/backups/paperless-files-$(date +\%Y-\%m-\%d).tar.gz -C ~/paperless-ngx data media

Step 6: Implement Health Checks and Restart Policies

Every service should have a health check and a restart policy. We already added a health check for the database. For the app, add the following:

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

Set restart: unless-stopped on all services, which we already did. This ensures containers restart automatically after a crash or a host reboot.

Step 7: Secure the Docker Socket

Exposing the Docker socket to containers is a security risk. If a container is compromised, the attacker gains root access to the host. Use the read_only and cap_drop options where possible. Add these to your app service:

    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

Step 8: Set Up Log Rotation

Logs can fill up your disk quickly. Configure Docker's logging driver to rotate logs. Add this to each service:

    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

You can also set this globally in /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Restart Docker to apply:

sudo systemctl restart docker

Step 9: Create a Makefile for Common Tasks

Simplify your workflow with a Makefile in your project directory. Create it with:

nano Makefile

Add the following content:

.PHONY: up down logs ps pull backup

up:
	docker compose up -d

down:
	docker compose down

logs:
	docker compose logs -f

ps:
	docker compose ps

pull:
	docker compose pull

backup:
	docker compose exec db pg_dump -U paperless paperless > backup_$$(date +\%Y-\%m-\%d).sql

Now you can run make up, make logs, etc.

Step 10: Version Control Your Configurations

Initialize a Git repository to track changes to your Compose files. Do not commit the .env file.

cd ~/paperless-ngx && git init && git add docker-compose.yml Makefile && git commit -m "Initial commit"

Create a .gitignore file:

echo ".env" > .gitignore && echo "backups/" >> .gitignore && echo "data/" >> .gitignore

Step 11: Test Your Setup

Verify that all services are running and healthy:

docker compose ps

Check the logs for any errors:

docker compose logs app | tail -50

Visit https://docs.example.com in your browser to confirm the application is accessible through the reverse proxy.

Step 12: Monitor Resource Usage

Use htop or docker stats to monitor CPU and memory usage. This helps you identify which containers are resource hogs.

docker stats --no-stream

Advanced Configuration and Optimization

Reverse Proxy and SSL

We already covered Traefik above. If you prefer Nginx Proxy Manager, the configuration is similar. The key point is to always terminate SSL at the proxy and never expose container ports directly to the internet. Use internal networks in Docker to restrict access between containers.

Backup Strategy

A solid backup strategy has three layers: offsite, encrypted, and tested. Use restic or borgbackup to push encrypted backups to a remote server or cloud storage. The postgres-backup-local container we used handles the database dump. For files, consider using rclone to sync to a remote location.

Security Hardening (Optional)

The following settings are more aggressive and may break containers if applied blindly. Do not copy them verbatim—adapt them to each service.

Setting Effect Risk
read_only: true Makes the container filesystem read-only Apps that write to their own directory will fail unless you mount tmpfs or volumes.
cap_drop: [ALL] Removes all Linux capabilities Some apps need specific capabilities like CHOWN or NET_ADMIN. Test thoroughly.
security_opt: [no-new-privileges:true] Prevents privilege escalation May break apps that need setuid binaries.
pids_limit: 100 Limits the number of processes Apps that fork many processes may crash.

To check which user and group your container runs as, run:

docker compose exec app id

Run id -u && id -g on your host and verify against the image's documentation. The default user in the paperless-ngx image is paperless (UID 1000). Adjust file permissions accordingly.

Troubleshooting Common Issues

Error Cause Solution
Error response from daemon: driver failed programming external connectivity Port already in use Run sudo lsof -i :8000 to find the process and stop it, or change the host port mapping.
Container is unhealthy Health check failing Check logs with docker compose logs <service>. The app may not be responding on the expected port.
Permission denied when writing to volumes UID/GID mismatch Run id -u && id -g on your host and compare with the container's user. Use chown -R $(id -u):$(id -g) ./data on the host.
Could not resolve host: db Service name not available Ensure the depends_on directive is correct and the database container is running. Check with docker compose ps.
TLS handshake error from reverse proxy Certificate issue Check Traefik logs. Ensure your domain points to your public IP and port 443 is open.
Got permission denied while trying to connect to the Docker daemon socket User not in docker group Run sudo usermod -aG docker $USER and log out/in.

Conclusion

Setting up a homelab with Docker Compose is not just about getting services running—it's about building a system that survives updates, crashes, and your own mistakes. The best practices covered here—version pinning, secret management, health checks, and backups—are the foundation of a reliable setup. Start small, apply these patterns to one service, then scale to your entire stack.

Remember that security is a process, not a destination. Regularly update your images, audit your exposed ports, and test your backups. The time you invest in this discipline pays off when a service fails at 3 AM and you can restore it in minutes.

FAQ

Q: Should I use latest tag or pin a specific version? A: Always pin a specific version in production. The latest tag can change unexpectedly and break your setup. Use a specific version like paperless-ngx:2.11.0 after checking the official GitHub releases. For development, latest is acceptable, but be prepared for breakage.

Q: How do I update my containers without downtime? A: Use docker compose pull followed by docker compose up -d. This recreates containers with the new image. To avoid downtime, run at least two replicas of each service behind a load balancer or use rolling updates with docker compose up -d --scale app=2. For databases, use replication or accept a few seconds of downtime.

Q: Is it safe to expose the Docker socket to containers like Traefik? A: It's a trade-off. Traefik needs the socket to discover containers dynamically. Mitigate the risk by using the :ro (read-only) flag and running Traefik with no-new-privileges:true. Never mount the socket into containers you don't trust completely.

Q: How often should I back up my data? A: For a homelab, daily backups are sufficient for most services. Use a schedule that matches your data's importance. For critical databases, consider hourly incremental backups. Always test your backups by restoring to a separate environment.

Q: Can I run Docker Compose on a Raspberry Pi? A: Yes, but be mindful of ARM architecture. Not all images support ARM. Use docker manifest inspect <image> to check compatibility. Also, performance will be lower—expect 2-3x slower database queries compared to an x86 machine. Adjust your expectations and monitor resource usage.

AdSense — In-article (responsive)

Related Guides