Docker Compose for Self-Hosting: The Guide
Restart policy, networking, volumes, secrets, build vs pull, logs, and teardown — the seven decisions a self-hosted Docker Compose stack forces, mapped in one guide with a real annotated compose.yaml and links to a deep dive on each.

The third service went in with a copy-pasted docker run command, the fourth with a slightly different one, and the fifth crashed overnight with no restart policy at all — silent, until a phone check the next morning found it dead for nine hours. Nothing about that stack was random; every line had been decided once, badly, and never revisited. Compose doesn't fix bad decisions by itself. It just forces you to make each one on purpose, once, in a file you can actually read back.
TL;DR
- A self-hosted Compose stack forces seven real decisions: restart policy, networking, volume type, secrets handling, build vs pull, log discipline, and teardown command.
- Get any one of the seven wrong and the failure mode is the same shape every time: it looks fine until a reboot, a crash, or a teardown finds the gap.
- None of the seven has one universally correct answer — the right choice depends on whether a service is stateless, holds data, or runs once and exits.
- A single annotated compose file below shows all seven decisions applied together, not in isolation.
- Each decision has its own deep dive linked inline — this pillar is the map, not a replacement for them.
KEY-STAT: 7 — Decisions a Docker Compose file forces on every self-hosted stack, whether or not anyone chose them on purpose
Why Compose is the right default, and where it stops being enough
Docker Compose describes a stack declaratively: one YAML file lists every service, its image, its volumes, its network, its restart behavior. Run docker compose up and Compose reconciles reality to match that description — it doesn't matter whether a container already exists, is stopped, or never ran; Compose figures out what has to happen to get there. That's the opposite of a shell script full of docker run flags nobody remembers the reasoning behind six months later.
Compose stays the right tool for as long as everything runs on one host. The moment a homelab needs a service to survive a single machine dying — not just a container restarting — that's a different problem (Swarm or Kubernetes), and outside this guide's scope. For one host, which is most homelabs, Compose is not a stepping stone to something more serious. It's the serious tool.
The seven decisions, in one table
| Decision | Wrong default | What to pick instead | Covered in depth |
|---|---|---|---|
| Restart policy | No restart: line at all | unless-stopped for long-running services | restart policies |
| Networking | Every service on Compose's flat default bridge | One internal network per stack, one shared network for the proxy | networking |
| Volumes | Bind-mounting a database's data directory casually | Named volumes for engine data, bind mounts for configs | volumes and backups |
| Secrets | Plain values under environment: | env_file: at minimum, secrets: for anything sensitive | secrets vs .env |
| Build vs pull | Assuming up rebuilds a changed Dockerfile | up --build, or build --no-cache when the cache itself is suspect | build vs pull |
| Logs | Waiting until something's already broken to look | docker compose logs -f --tail as a standing habit, not a last resort | companion deep dive, in progress |
| Teardown | Reaching for down -v out of habit | Matching the command to whether data needs to survive | down vs stop vs kill |
Note
None of these seven is Compose-specific trivia — they're the same decisions Kubernetes forces too, just spread across more YAML. Getting them right in Compose first is what makes a later move to anything bigger legible instead of a rewrite from scratch.
Restart policy: what survives a reboot, on purpose
restart: decides what happens after a crash or a host reboot, and Compose's actual default — no policy at all — means nothing comes back automatically. unless-stopped is the right default for almost every long-running self-hosted service: it survives crashes and reboots but still respects a deliberate docker compose stop. always looks identical until someone stops a container on purpose and the Docker daemon restarts anyway, bringing it right back — a genuine surprise the first time it happens. The full breakdown of all four policies, including which one fits a one-shot backup job versus a service that must never stay down, is in the restart policies guide.
Networking: isolated by default, exposed on purpose
Every docker compose up creates its own bridge network and puts every service in that file on it — which is exactly right within one stack, and exactly wrong the moment a reverse proxy needs to reach several separate stacks. This homelab's actual layout: each stack (media, credentials, automation) gets its own private network that never leaves its compose file, and Caddy joins one additional shared network that every stack also joins — nothing in one stack can reach anything in another, and only the proxy bridges them. The networking guide covers the internal: true and external: true flags that make that layout work.
Volumes: the one decision that can lose data
Bind mounts map a specific host folder into a container — direct, inspectable with a plain ls, and the right choice for configs and media. Named volumes live under Docker's own storage path and are the right choice for database engine data, because a database expects consistent filesystem semantics a bind mount doesn't always guarantee. The distinction matters most at teardown: docker compose down -v deletes every named volume declared in that file, immediately, with no confirmation prompt — the near-miss that prompted the volumes and backups guide is a genuine thirty seconds of not knowing whether a year of data still existed.
Warning
docker compose down -v is the single most dangerous command in this entire guide. It's also the easiest one to type by muscle memory from a different project where -v was harmless. Confirm which volumes a compose file declares before ever running it with that flag.
Secrets: keeping credentials out of a file you might git add
environment: is the path of least resistance and the easiest way to leak a credential — the value sits in the compose file itself and shows up in plain text under docker inspect. env_file: moves the value into a separate, gitignored file, which stops the accidental commit but doesn't stop docker inspect from still showing it once the container is running. The secrets: block, standalone in Compose v2 and not Swarm-only, mounts a value as a read-only file instead — real ceremony for a single host, but the only one of the three that never appears in an environment dump. Full comparison, including where each option actually leaks, in the secrets vs .env files guide.
Build vs pull: making sure a rebuild actually rebuilds
Plain docker compose up never checks whether a Dockerfile changed — it reuses whatever image is already tagged locally, silently. up --build rebuilds but still respects Docker's layer cache, so only the layers from the first changed instruction onward actually re-run; build --no-cache throws the cache away entirely and is the only one guaranteed to reflect every change, at the cost of being the slowest. The build vs pull guide has the actual rebuild-time numbers and the volume-masking trap that can make a correct rebuild still look stale.
Logs: the habit that turns a mystery into a two-minute fix
docker compose logs -f --tail=100 <service> is the first command to run the moment anything looks wrong — before restarting the container, before touching the compose file, before guessing. Restarting first is the single most common mistake: it clears whatever was in the container's stdout buffer and destroys the one piece of evidence that would have explained the crash. A dedicated deep dive on reading Compose logs like a debugging session, not a wall of scrolling text, is in progress as the next spoke in this series.
Teardown: stop, down, down -v, and kill are four different commands
docker compose stop pauses containers and leaves the network and volumes in place — the fastest path back with start. down additionally removes the containers and the default network, but still leaves named volumes untouched. down -v is the only one of the four that deletes data. kill skips the graceful SIGTERM grace period entirely and should be reached for only when a container is already known to be unresponsive. The down vs stop vs kill guide has the full decision table and the SIGTERM-then-SIGKILL timing behind it.
A real compose file, with all seven decisions annotated
This is the actual shape of the media stack on this homelab — Jellyfin behind Caddy, each decision from the table above visible in the file itself rather than left implicit:
services:
jellyfin:
image: jellyfin/jellyfin:10.10
restart: unless-stopped # decision 1 — long-running service, respects manual stop
networks:
- media-internal # decision 2 — private network, only this stack sees it
- proxy-shared # decision 2 — the only bridge to the reverse proxy
volumes:
- ./jellyfin/config:/config # decision 3 — bind mount, directly inspectable configs
- media-library:/media # decision 3 — named volume for the actual media data
env_file:
- .env # decision 4 — credentials out of the compose file itself
caddy:
image: caddy:2.9
restart: unless-stopped
networks:
- proxy-shared # decision 2 — joins every stack's shared network, nothing else
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- caddy-data:/data
networks:
media-internal:
internal: true # decision 2 — no outbound internet access from this network
proxy-shared:
external: true # decision 2 — shared across stacks, survives any single down
volumes:
media-library:
caddy-data:
Every one of the seven decisions is visible without opening a second file: unless-stopped for a service that should stay up, two networks doing two different jobs, a named volume reserved for the data that actually needs backing up, and env_file: keeping the .env this repo's .gitignore already excludes. Building or rebuilding this image is a plain docker compose pull here — nothing in this stack has a local Dockerfile, so build vs pull isn't even a live decision for it, which is itself the correct outcome of that decision tree.
Tip
Copy the table near the top of this guide into a project's README once, with the actual choice made for each service. Six months later, "why is this one always and that one unless-stopped" stops being a mystery you have to reverse-engineer from the file itself.
The bottom line
A Compose file doesn't have a "correct" restart policy, network layout, or teardown command baked in — it just makes each of the seven decisions visible, in one place, instead of scattered across a shell history nobody kept. Getting all seven right the first time, for every new service, is what separates a homelab that survives its own reboots from one that only looks stable because it's never been tested. Start from the homelab beginner's guide if the stack doesn't exist yet, then work through each of the seven spokes above as the services get added.
Frequently asked questions
Is Docker Compose still worth learning in 2026?
Yes — it remains the default way to describe and run a multi-container stack on a single host, and the seven decisions in this guide (restart, networking, volumes, secrets, build, logs, teardown) are unavoidable in any tool that replaces it, including Kubernetes.
What is the difference between Docker Compose and Kubernetes for a homelab?
Compose reconciles a stack's state on one host; Kubernetes reconciles it across many. A homelab running on a single machine gains little from Kubernetes' complexity and loses the simplicity of a single readable YAML file — Compose stays the better fit until a service genuinely needs to survive one physical machine failing.
Do I need Docker Swarm if I'm already using Compose?
Not for a single-host homelab. Swarm adds multi-host orchestration Compose doesn't have, but the secrets: block and most of Compose's v2 features work standalone, without Swarm mode enabled at all.
How many services is too many for one Compose file?
There's no hard number, but a single file mixing unrelated stacks (media, credentials, automation) forces them onto the same default network and the same lifecycle — splitting into one compose file per stack, joined only through a shared proxy network, keeps them isolated and lets each one be torn down independently.
Is Docker Compose secure enough for self-hosted production use?
It can be, but security comes from the seven decisions in this guide being made deliberately — network isolation, a real secrets strategy, and a restart policy that doesn't mask failures — not from Compose itself. None of the seven is secure by default; all seven are securable by choice.
More from Self-Hosting & Privacy

A fixed triage order for a Docker Compose service that won't start: docker compose ps, logs --tail, the exit code, and the healthcheck status -- in that order, before restarting anything, plus how to tell a real crash loop from a slow first boot.

I pasted a fresh Postgres password straight into environment: on a new compose file, then stopped with my cursor over git add . That password was about to sit in plain text in a repo I push to self-hosted Gitea, not because I was careless, but because environment: is the path of least resistance.

I edited a Dockerfile, ran docker compose up -d, and the container came back up running the exact same code it had before. No error, no warning, just silence, and a service that looked started but was not the one I had just changed.
Stay in the loop
Get the latest articles delivered to your inbox. No spam, unsubscribe anytime.
Debugging a Broken Docker Compose Stack
A fixed triage order for a Docker Compose service that won't start: docker compose ps, logs --tail, the exit code, and the healthcheck status -- in that order, before restarting anything, plus how to tell a real crash loop from a slow first boot.
Continue Reading