Back to Blog
team@tinypod.app

Docker Compose Best Practices for Self-Hosting

Docker Compose is the backbone of self-hosted stacks. These best practices keep your services reliable, secure, and maintainable.

dockerdocker-composebest-practices

Use Specific Image Tags


Never use :latest in production.


Bad: image: postgres:latest

Good: image: postgres:16.2-alpine


Pinned versions mean predictable behavior. You update when you choose to, not when upstream pushes a breaking change.


Restart Policies


Always set a restart policy:

restart: unless-stopped


Options:

  • no: Never restart (testing only)
  • always: Restart no matter what
  • unless-stopped: Restart unless you explicitly stopped it
  • on-failure: Restart only on non-zero exit codes

  • For most self-hosted apps: unless-stopped.


    Health Checks


    healthcheck:

    test: ["CMD", "curl", "-f", "http://localhost:8080/health"]

    interval: 30s

    timeout: 5s

    retries: 3

    start_period: 60s


    Health checks tell you (and other services) when an app is actually ready.


    Network Isolation


    Don't put everything on the default network. Create separate networks:


    networks:

    frontend:

    backend:


    services:

    app:

    networks: [frontend, backend]

    database:

    networks: [backend]

    caddy:

    networks: [frontend]


    The database is only reachable by the app, not by the reverse proxy.


    Environment Variables


    Don't hardcode secrets in docker-compose.yml.


    Use .env files:

    env_file:

  • .env

  • Or Docker secrets for sensitive values.


    Resource Limits


    deploy:

    resources:

    limits:

    memory: 512M

    cpus: '0.5'


    Prevent a single runaway container from consuming all server resources.


    Logging


    Configure log rotation to prevent disk exhaustion:

    logging:

    driver: json-file

    options:

    max-size: "10m"

    max-file: "3"


    Without this, a chatty application can fill your disk.


    Volume Organization


    Use named volumes with clear names:

    volumes:

    postgres_data:

    app_uploads:

    redis_data:


    Never use anonymous volumes in production.


    Ordering with depends_on


    depends_on:

    db:

    condition: service_healthy


    Combine depends_on with health checks so your app waits for the database to be actually ready, not just started.