Self-Hosting
A maintainable Docker Compose home stack
Structure a Compose stack that survives reboots, updates, and a rebuild — restart: unless-stopped, .env out of git, named volumes for data, pinned image digests instead of latest, and a backup that captures config + data together.
Problem summary
A home stack should survive three events unattended: a host reboot, an image update, and a full rebuild on new hardware. The failure modes are predictable — containers that don't come back (no restart policy, or the Docker daemon isn't enabled on boot), data that vanished because it lived in the container layer instead of a volume, secrets committed to git, and 'auto-update everything' tooling that silently pulls a breaking `latest` overnight. The maintainable answer is one declarative `compose.yaml` with `restart: unless-stopped`, secrets in a git-ignored `.env`, named volumes or owned bind mounts for data, images pinned to a tag+digest (not `latest`), healthchecks wired to `depends_on`, and a backup that captures the compose files + `.env` + the data together.
Check each service has restart: unless-stopped.
docker compose config
Services are set to restart unless-stopped.
A backup that doesn't restore isn't a backup.
Layer path
Step-by-step runbook
Start here. Do each check in order, compare it to the expected result, and stop when the evidence explains the failure or the safe stop point applies.
Standardize the file
Check: Use a single compose.yaml with the Compose v2 plugin.
Expected result: docker compose config resolves cleanly.
If not: Migrate legacy docker-compose.yml/binary usage.
Set restart + boot
Check: Add restart: unless-stopped and enable docker on host boot.
Expected result: The stack returns after a reboot.
If not: If not, the daemon isn't enabled at boot.
Persist data deliberately
Check: Move app/db data into named volumes or owned bind mounts.
Expected result: Data survives recreate and update.
If not: Anything still in the container layer will be lost.
Externalize secrets
Check: Put secrets in a git-ignored .env; commit a blank .env.example.
Expected result: No secrets in git; .env interpolates into compose.
If not: Rotate anything already committed.
Pin and gate
Check: Pin image tag+digest; add healthchecks + depends_on service_healthy.
Expected result: Updates are deliberate and start order is correct.
If not: Never auto-update databases; scope Watchtower if used.
Back up the whole stack
Check: Back up compose files + .env + volume/bind-mount data together.
Expected result: You can rebuild the stack on new hardware.
If not: Prove it by restoring to a scratch host.
Safe stop: A backup that doesn't restore isn't a backup.
Decision tree
If: Containers don't return after a reboot
Then: No restart policy or the daemon isn't enabled at boot.
Action: Set restart: unless-stopped and enable the docker service on the host.
If: Data disappeared after an update/recreate
Then: It lived in the container layer.
Action: Move it to a named volume or owned bind mount and recreate.
If: An overnight update broke a service
Then: Floating latest + unattended auto-update.
Action: Pin tag+digest; scope Watchtower with --label-enable; never auto-update DBs.
Safe stop: Roll back to the pinned digest you know worked.
If: Dependent service races its dependency
Then: depends_on without a health condition.
Action: Add a healthcheck and depends_on: condition: service_healthy.
If: Can't rebuild the stack elsewhere
Then: Backup missing the compose files, .env, or the data.
Action: Back up all three together and prove the restore on a scratch host.
Evidence table
| Symptom | Evidence to collect | Likely layer | Next action |
|---|---|---|---|
| Stack is down after a power blip | Service restart policy + host docker.service enabled state | Missing restart policy / daemon not enabled | restart: unless-stopped + enable docker on boot. |
| Fresh-looking app after docker compose up | Where the data was mapped (volume vs container layer) | Data in the container layer | Relocate data to a volume/bind mount. |
| Secrets showing in the git repo | git status / git log for .env | .env committed | git-ignore it; rotate exposed secrets. |
| Service broke after an unattended update | Image tag (latest?) and Watchtower scope | Uncontrolled image drift | Pin tag+digest; scope/disable auto-update. |
| Service starts before its database is ready | depends_on condition + healthcheck presence | No readiness gating | Add healthcheck + service_healthy. |
Commands and settings paths
Validate and view the resolved config
docker compose config
Where: In the project folder (Compose v2 plugin)
Expected: The merged config prints with variables resolved and no errors.
Failure means: A parse/interpolation error means the file or .env is wrong.
Safe next step: Fix before deploying; this is also your sanity check after edits.
Confirm restart policies are applied
docker inspect -f "{{.Name}} {{.HostConfig.RestartPolicy.Name}}" $(docker compose ps -q)
Where: In the project folder
Expected: Each service reports unless-stopped.
Failure means: A 'no' policy means the service won't survive a reboot.
Safe next step: Set restart: unless-stopped and recreate.
Back up a named volume
docker run --rm -v <volume>:/data -v "$PWD":/backup alpine tar czf /backup/<volume>.tgz -C /data .
Where: On the Docker host
Expected: A tarball of the volume's contents is produced.
Failure means: An empty/error tarball means the volume name is wrong or data is bind-mounted elsewhere.
Safe next step: Pair volume backups with the compose files and .env.
Hardware and platform boundary
Change only when
- Adopt pinned digests + a deliberate update process once the stack runs anything you depend on daily.
- Add healthchecks when start-order races start causing flaky boots.
Evidence that matters
- restart: unless-stopped + the daemon enabled on boot.
- Named volumes / owned bind mounts for all persistent data.
- A git-ignored .env and pinned image tags/digests.
Evidence that does not matter
- Clever orchestration (k8s, swarm) for a handful of home containers.
- Auto-updating to latest for the feeling of being current.
Avoid
- Storing data in the container layer.
- Committing .env to git.
- Unattended Watchtower on latest across the whole stack (especially databases).
Related tool
Use the linked tool to turn this runbook into a guided check for your exact setup.
NAS setup plannerRelated problems
Last reviewed
2026-06-03 · Reviewed by HomeTechOps. Built from 2026-06 research verified against the Docker Compose specification, the docker compose CLI reference, Docker's build best-practices (image pinning), the volumes docs, and the environment-variable precedence docs. The operator differentiators are designing for reboot/update/rebuild explicitly and treating pinned digests + a restore-proven backup as the baseline, not Watchtower-on-latest.
Sources/assumptions
- Assumes Docker Engine with the Compose v2 plugin (invoked as `docker compose`, not the legacy `docker-compose` binary) on a Linux host or a NAS that runs standard Docker.
- Filename and syntax follow the current Compose Specification: `compose.yaml` is preferred (legacy `docker-compose.yml` still works); image-pinning and volume-backup guidance are from Docker's own docs.
- Watchtower auto-update is described as a risk to scope, not a recommendation — Docker's guidance is to pin tags/digests and update deliberately; databases should not be auto-updated.
Source-backed checks
HomeTechOps turns official docs and conservative safety rules into a shorter runbook. These links are the source trail for the page direction.