Back to Log
2026 — FEB 08 Docker DevOps 6 MIN READ

The Art of the Minimal Docker Compose Stack

Less is more when orchestrating containers. My philosophy on keeping compose files readable, reproducible, and resistant to configuration drift.

I've seen compose files that span three hundred lines, define seventeen networks, and require a dedicated README just to explain the environment variables. I've also written a few of them. That era is over. Every stack I deploy now follows a small set of principles that I've refined through a lot of painful rollbacks.

The Problem with Bloat

Configuration drift is silent and cumulative. A commented-out service here, an undocumented environment variable there — and six months later, no one (including you) knows what's load-bearing and what's vestigial. The compose file stops being documentation and becomes archaeology.

If you can't redeploy your stack from scratch in under ten minutes, your compose file has already drifted past the point of trustworthiness. — Personal notes, 2025

My Principles

These aren't rules I found in a book. They're the residue of things going wrong in production — or what passes for production in a home lab.

  • One service, one file. Don't bundle unrelated services into a single compose. If they don't share a network, they don't share a file.
  • Pin your tags. latest is a liability. Always pin to a specific version and update deliberately.
  • Named volumes over bind mounts for persistence. Bind mounts are fine for config; named volumes are for data you can't afford to lose.
  • Restart policy by default. Every service that should survive a reboot gets restart: unless-stopped.
  • Networks are explicit. If a service doesn't need to talk to another, it doesn't go on the same network. Principle of least connectivity.

A Real Example

Here's what a clean, minimal stack looks like in practice — this is my Vaultwarden deployment:

services:
  vaultwarden:
    image: vaultwarden/server:1.30.5
    restart: unless-stopped
    environment:
      DOMAIN: "https://vault.yourdomain.com"
      SIGNUPS_ALLOWED: "false"
    volumes:
      - vw-data:/data
    networks:
      - proxy

volumes:
  vw-data:

networks:
  proxy:
    external: true

That's it. No port mappings — NPM handles ingress. No custom build steps. No environment file for three variables. The intent is readable at a glance, and I could redeploy this from memory if I had to.

On Environment Files

.env files are powerful and easy to misuse. I use them only when a service has more than four or five variables, or when values need to stay out of version control. For anything smaller, inline environment variables in the compose file are clearer — you see the config and the service definition in the same place.

What I never do: commit .env files to git. Not even with placeholder values. Use a .env.example instead and document every variable.

Lessons Learned

The compose file isn't just an operational artifact — it's documentation. Treat it like code you'll have to read at 2am during an outage, because you will. Every line should justify its existence. If you're not sure what a setting does, that uncertainty is a signal to either understand it or remove it.


Minimalism in infrastructure isn't laziness. It's the discipline to resist adding complexity until complexity is genuinely required. Most of the time, it never is.

// END OF TRANSMISSION — T. SEUGLING
← Back to Log