Self-hosting with Docker Compose: lessons learned
I have been self-hosting various services with Docker Compose for a while now. Nextcloud, databases, monitoring tools, small web apps. Along the way I have picked up some patterns that work and made some mistakes worth sharing.
Keep your compose files focused
Early on I had one massive docker-compose.yml with every service crammed in. Database, web server, cache, monitoring, backups, all in one file. It worked, but updating one service meant risking the entire stack.
Now I split things into separate compose files per logical group. The database and its backup tool live together. The web app and its reverse proxy live together. Each group can be updated, restarted, or debugged independently.
Always pin image versions
Using image: postgres:latest is asking for trouble. One day you run docker compose pull and suddenly your Postgres jumps a major version and your data is incompatible. Pin to specific versions like postgres:16.2 and update intentionally.
Volumes for data, bind mounts for config
Use named Docker volumes for data that the container manages (database files, application state). Use bind mounts for configuration files you want to edit on the host. This keeps the separation clean and makes backups straightforward.
services:
postgres:
image: postgres:16.2
volumes:
- pgdata:/var/lib/postgresql/data
- ./postgres.conf:/etc/postgresql/postgresql.conf
volumes:
pgdata:Set up health checks
Docker health checks are easy to add and save a lot of debugging time. Without them, Docker considers a container "running" even if the application inside crashed on startup. With health checks, dependent services wait until the dependency is actually ready.
services:
postgres:
image: postgres:16.2
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5Use .env files for secrets
Do not hardcode passwords and API keys in your compose file. Use a .env file that is excluded from version control. Docker Compose reads it automatically.
services:
app:
environment:
- DATABASE_URL=postgres://user:${DB_PASSWORD}@postgres:5432/myappSet restart policies
Every long-running service should have restart: unless-stopped or restart: always. Without it, a server reboot means manually starting every container again.
Back up your volumes
Named volumes are great, but they are just directories on the host filesystem. Set up a cron job or a backup container that regularly copies volume data to an external location. I have seen people lose months of data because they assumed Docker volumes were somehow protected.
The logging problem
Docker containers produce logs. A lot of logs. By default they are stored as JSON files that grow without limit. Set a log rotation policy in your daemon config or per-container:
services:
app:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"Keep it simple
The temptation with self-hosting is to keep adding services. A monitoring stack, a log aggregator, a dashboard for the dashboard. Every service adds maintenance burden. Only self-host things you actually use and benefit from running yourself. For everything else, a managed service is usually worth the cost.
Sources
Related posts
Automating workflows with n8n
How I use n8n as a self-hosted alternative to Zapier for connecting services and automating repetitive tasks.
How Docker image layers actually work under the hood
A deep dive into Docker image layers, union filesystems, content-addressable storage, copy-on-write, and why understanding this stuff makes you better at writing Dockerfiles.
Containers vs virtual machines: when to use which
A practical comparison of Docker containers and virtual machines, and how I use both in my homelab.
Enjoying the blog? Subscribe via RSS to get new posts in your reader.
Subscribe via RSS