SelfhostRealm
Isometric beige platform with a server, database cylinder, tunnel gateway and container linked by blue pipes, plus spare crates, a wrench and a gear nearby.
guides

How to Decide What Goes in Your Docker Compose Stack

Compose makes adding a service nearly free to start and expensive to keep. A framework for deciding which services earn a lasting place in your stack.

By SelfhostRealm Editorial · ·Updated August 22, 2026 · 6 min read

Adding a service to a Compose file takes about ninety seconds. That is the problem.

The syntax is easy enough that most home stacks grow by accident. Something looked interesting on a Sunday afternoon, it went into the file, and two years later it is still running: unpatched, holding data nobody has looked at, occupying a port you have forgotten about. The file grew, but nobody ever decided anything.

This is not a walkthrough of Compose syntax. It is about the decision that comes before the YAML. Which services actually belong in your stack, how you should group them, and what each one will cost you for as long as you keep it.

Every service block is a standing obligation

The moment you add nine lines of YAML, you have signed up for all of the following, indefinitely:

  • Image updates, and the breaking changes that occasionally ride along with them
  • A backup that has to be verified, not merely scheduled
  • Credentials that will eventually need rotating
  • A port, a proxy route, and a DNS name that now conflict with something else
  • Your attention when it breaks at eleven at night

Compose hides every one of those costs at the moment you add the service, and none of them afterwards. That asymmetry is what turns a tidy stack into a haunted one.

Sort by consequence, not by category

Before writing a service block, put the service into one of three tiers. The tier is the only piece of metadata in the file that actually drives decisions.

Disposable. No state you would grieve. Dashboards, speedtest trackers, a feed reader whose subscriptions you could re-import in ten minutes. If the container vanished you would shrug and redeploy.

Convenience. Real state, but a commercial fallback exists and downtime is merely irritating. A media server sits here for most people, whether you landed on Jellyfin or Plex.

Load bearing. Other people in the household depend on it, or your instance holds the only copy of something. Password vaults, photo libraries, household documents, DNS. A bad afternoon here is not a hobby setback, it is somebody unable to get into their bank account.

Write the tier in a comment above the service. Restart policy, backup frequency, update cadence, and which file the service lives in all fall out of that one word.

Four questions before the service earns a block

What breaks if this is offline for a week? If the honest answer is nothing, treat it as disposable and stop over-engineering it. If the answer involves another person, you have taken on an obligation with a human at the other end of it.

Can you point at the state in one sentence? Name the directory or volume that holds the data. If you cannot, you cannot back it up, and the service is not really self-hosted so much as self-hoarded.

What is the exit? Does the project export in a format something else can read, or will it only ever hand you its own database dump? Lock-in is an acceptable choice. Discovering it during a migration is not.

Who publishes the image? Check whether it is the project’s official image or one person’s rebuild, when it was last pushed, and whether the project cuts versioned tags at all. An image with nothing but a permanently moving latest tag is telling you something about how the upstream treats stability.

Split the file by blast radius

The usual advice is one file for everything. That works at three services. At fifteen, docker compose down has quietly become an all-or-nothing lever over your entire house, and adding a toy means restarting the thing your family depends on.

Split along the tiers instead: ~/stacks/core/, ~/stacks/media/, ~/stacks/toys/. Each directory gets its own Compose file and its own lifecycle. Services that genuinely need to reach each other join a shared network you create once, outside any stack:

networks:
  edge:
    external: true

Then each service that needs to be reachable by the reverse proxy joins edge, and everything else stays on its own stack-local network, unreachable from the rest of the house. Grouping becomes a security boundary rather than an accident of file order.

The volume line is your backup contract

For anything load bearing, prefer a bind mount to a named volume. Not because named volumes are worse technically, but because a bind mount is a path you can see, copy, inspect, and restore without Docker being involved at all. Data you cannot find on the host filesystem is data you do not fully own, which is the whole point of doing this at home.

Then make enrollment automatic rather than remembered. Label the service:

    labels:
      - "backup=daily"

and have the backup job select by label instead of by a hand-maintained list of paths. A new service is protected the day it is added, not the day you remember it exists. Pair that with a real 3-2-1 backup strategy for the load-bearing tier, because a copy on the same disk is not a backup.

Decide who controls the timing of change

Floating on latest means you have delegated the scheduling of your breaking changes to a stranger on the internet. That is a fine trade for a dashboard. It is a poor one for a database that other people depend on.

Pin load-bearing services to a major version tag and update deliberately, on a day you chose, with the backup verified beforehand. Let disposable services float freely. Databases deserve particular care: a container that silently jumps a major version on restart can leave you with an on-disk format the previous image will refuse to open.

Prune on a schedule

Once a quarter, walk the stack and ask one question per service: have I opened this in ninety days? If not, bring it down, archive its data directory, and delete the block. Something you never look at is not a capability, it is an unattended attack surface with a maintenance bill.

A stack small enough to hold in your head is worth more than a stack that has everything.

The rebuild test

Before you call a service load bearing, prove it can come back. On a spare machine or a throwaway VM, check out the Compose directory, restore the data from backup, and bring it up. If it does not return, what you had was a running container rather than a self-hosted service, and the difference only becomes visible on the worst possible day. Repeat annually for the top tier, or after any change to how backups run.

What should stay out of the file

  • Anything that must survive the host dying without you intervening. Compose restarts containers, not hardware, and a single mini PC or Pi is a single point of failure by design.
  • Secrets pasted into environment: for load-bearing services. Those values are visible to anything that can run docker inspect and often end up in logs. Use an env_file kept out of version control, with permissions set accordingly.
  • Services that fight the host for a port or a system role. DNS is the classic case, and it is usually happier on its own box than sharing a stack.

The file is a statement of what you have chosen to own

Read your Compose file as a list of commitments rather than a list of features. Every block is something you have promised to patch, back up, and eventually migrate. Most stacks would be better if a third of the entries were deleted and the rest were taken seriously.

If you are still working out what belongs there in the first place, start from the first five services worth running, then widen the menu with the self-hosted alternatives to Google Workspace once the maintenance rhythm feels sustainable.

#docker-compose #self-hosting #planning#maintenance#containers

Related