Docker Patterns
Operational lessons from running 25+ containers — what works, what breaks, and what to do about it
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.