DevOps

Docker in Production: Lessons from the Field

Real-world patterns for running Docker in MSP environments — networking gotchas, volume management, and keeping containers healthy.

Hassan Bouba·May 10, 2026

Running Docker on your laptop is easy. Running it reliably across dozens of client environments at an MSP is a different game entirely. Here's what I've learned the hard way.

Network Modes: Don't Default to Bridge

The default bridge network works fine for single-host development. In production you'll hit its limits fast:

  • Container-to-container DNS only works within the same user-defined network
  • The default bridge network doesn't support DNS resolution by container name
  • Port mapping overhead adds latency

What I use instead:

For services that need to talk to each other: always create user-defined bridge networks. DNS resolution by service name works out of the box.

docker network create app-network
docker run -d --name postgres --network app-network postgres:16
docker run -d --name api --network app-network myapi:latest
# "api" container can reach postgres at hostname "postgres"

For services that need host-level performance (e.g., a Prometheus node exporter): use --network host. You lose network isolation but gain full throughput.

Volume Management

Volumes are where most production Docker deployments get messy. My rules:

Always use named volumes, never bind mounts for application data.

Bind mounts couple your container to the host filesystem path. Named volumes are managed by Docker and survive container recreation.

# docker-compose.yml
services:
  db:
    image: postgres:16
    volumes:
      - postgres_data:/var/lib/postgresql/data  # named volume ✓
      # NOT: - ./data:/var/lib/postgresql/data  # bind mount ✗

volumes:
  postgres_data:

Exception: config files and secrets. Bind-mounting a config file from /etc/myapp/config.yaml is fine and makes editing straightforward.

Backup Strategy

Named volumes don't back themselves up. My approach for client environments:

# Dump to a tarball on the host
docker run --rm \
  -v postgres_data:/data \
  -v /backup:/backup \
  alpine tar czf /backup/postgres_data_$(date +%Y%m%d).tar.gz -C /data .

Pair this with a cron job and offsite sync (rsync to a NAS or Backblaze B2).

Health Checks: Stop Relying on "Running" Status

A container can be "running" and completely broken. Always define a HEALTHCHECK:

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

In Compose:

services:
  api:
    image: myapi:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

With health checks defined, docker ps shows healthy/unhealthy status, and dependent services (via depends_on: condition: service_healthy) won't start until dependencies pass.

Restart Policies

Every production container should have a restart policy. I use unless-stopped as my default — it survives reboots but doesn't fight you when you deliberately stop a container for maintenance.

services:
  app:
    image: myapp:latest
    restart: unless-stopped

Avoid always in most cases — if a container crashes in a tight loop, always will hammer your host. unless-stopped combined with health checks gives you the right behavior.

Logging

The default json-file driver is fine until your logs fill the disk. Set limits:

services:
  app:
    image: myapp:latest
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "5"

For centralised logging across clients, I ship to a Loki instance via the loki logging driver. One Grafana dashboard covers everything.

Compose in Production

docker compose is underrated for production on single hosts. It gives you:

  • Declarative service definitions in version control
  • docker compose pull && docker compose up -d for zero-downtime-ish updates
  • Easy rollback by pinning image tags

For multi-host orchestration, Swarm or Kubernetes come into play — but most MSP client workloads don't need that complexity. A well-configured single host with Compose handles more than you'd expect.


The biggest lesson: treat your Docker setup like infrastructure code. Version control your Compose files, document your volume layout, and test your backup restores. The failure mode isn't usually Docker itself — it's the operational practices around it.

All postsGet in touch