Skip to main content
Back to blog

Self-hosting with Docker Compose: lessons learned

·3 min readDevOps

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: 5

Use .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/myapp

Set 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

Enjoying the blog? Subscribe via RSS to get new posts in your reader.

Subscribe via RSS