HomeTechOps

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.

Operator snapshotEvidence first
First proof

Check each service has restart: unless-stopped.

Screen to open

docker compose config

Expected signal

Services are set to restart unless-stopped.

Stop boundary

A backup that doesn't restore isn't a backup.

Layer path

1A home stack must survive three events unattended: a reboot, an image update, and a rebuild on new hardware. Each has a predictable failure mode you design out up front.
2Reboot survival = restart: unless-stopped AND the Docker daemon enabled on boot. Data survival = volumes/owned bind mounts, never the container layer. Secret safety = a git-ignored .env.
3Update safety = pin a tag+digest and update deliberately; floating latest plus unattended Watchtower can ship a breaking version overnight with no rollback, and databases must never be auto-updated.
4Rebuild = back up the compose files + .env + the actual volume/bind-mount data together; the containers themselves are disposable because they rebuild from images.
Runbook

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.

1

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.

2

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.

3

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.

4

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.

5

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.

6

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

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

Evidence table

SymptomEvidence to collectLikely layerNext action
Stack is down after a power blipService restart policy + host docker.service enabled stateMissing restart policy / daemon not enabledrestart: unless-stopped + enable docker on boot.
Fresh-looking app after docker compose upWhere the data was mapped (volume vs container layer)Data in the container layerRelocate data to a volume/bind mount.
Secrets showing in the git repogit status / git log for .env.env committedgit-ignore it; rotate exposed secrets.
Service broke after an unattended updateImage tag (latest?) and Watchtower scopeUncontrolled image driftPin tag+digest; scope/disable auto-update.
Service starts before its database is readydepends_on condition + healthcheck presenceNo readiness gatingAdd healthcheck + service_healthy.
Reference

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 boundary

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 planner

Related 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.