These are patterns learned from running 25+ Docker containers on a homelab for years. Not theory — operational reality. The things that saved me at 3am and the things that caused the 3am in the first place.

Organization

One service, one directory

~/services/
├── traefik/
│   ├── docker-compose.yml
│   ├── config/
│   └── certs/
├── vaultwarden/
│   ├── docker-compose.yml
│   ├── .env
│   └── data/
├── gitea/
│   ├── docker-compose.yml
│   └── data/
└── ...

Every service gets its own directory with its own compose file. This lets you docker compose up -d and docker compose down per service. At 25+ services, a single monolithic compose file becomes a nightmare — one bad change takes everything down, and docker compose logs becomes an unreadable wall.

Version control the configs, not the data

Your compose files, environment templates, and config files go in Git. Your data volumes do not. The compose files are replaceable — clone the repo and redeploy. The data is not — that's what backups are for.

Keep a .env.example alongside each .env with placeholder values. Don't commit real secrets.

Networking

Use a shared proxy network

docker network create proxy

Every service that needs to be reachable through Traefik joins the proxy network. Services that only talk to each other (like an app + its database) share a private network. This gives you isolation where you need it and connectivity where you don't.

services:
  app:
    networks:
      - proxy
      - internal
  db:
    networks:
      - internal

networks:
  proxy:
    external: true
  internal:

The database is unreachable from the proxy network. The app is reachable from both.

Don't expose ports you don't need

If a service is behind Traefik, it doesn't need a ports: mapping. Traefik reaches it through the Docker network. Exposing ports means those services are also reachable by IP:port, bypassing your reverse proxy and its security.

Only expose ports for services that need direct access — like a game server or a signaling server.

Data management

Named volumes for databases, bind mounts for configs

volumes:
  - postgres_data:/var/lib/postgresql/data  # named volume
  - ./config/app.conf:/etc/app/app.conf:ro  # bind mount, read-only

Named volumes are managed by Docker and survive docker compose down. Bind mounts give you direct filesystem access for config files you want to edit without entering the container.

Back up before updating

docker compose down
cp -r ./data ./data.bak.$(date +%Y%m%d)
docker compose pull
docker compose up -d

If the update breaks something, you have the data from before the pull. Test this restore path before you need it.

Lifecycle

Restart policies

restart: unless-stopped

Use this for everything. If the host reboots, the service comes back. If you explicitly stop a container with docker compose stop, it stays stopped. always will restart containers you intentionally stopped, which is rarely what you want.

Health checks

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

Health checks let Docker know whether your service is actually working, not just running. Dependent services can wait for health before starting. Monitoring can alert on unhealthy containers.

Log management

Docker logs grow unbounded by default. Set limits:

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

Without this, a chatty service will fill your disk. Ask me how I know.

Updates

Don't run latest tags in production and forget about them. Know what version you're running. Pin versions when stability matters. Pull and test updates deliberately.

For a structured approach to tracking what needs updating across your homelab, see RedFlag — that's literally why I built it.

The meta-pattern

The real pattern isn't any individual technique. It's treating your homelab like infrastructure, not a hobby. Version control your configs. Back up your data. Monitor your services. Document what you did and why.

It's the same discipline whether you're running 3 containers or 300. Start with it early and it scales.