Skip to content
Docker Compose for Homelab Beginners
DWG-40538
MOD-09 · Self Hosting

Docker Compose for Homelab Beginners

Your first multi-container stack, explained end to end

Siddh Mistry Siddh Mistry November 11, 2026 Updated November 11, 2026 3 min read
Designer
Siddh Mistry
Module
MOD-09 — Self Hosting
Runtime
3 min read
Documentation Length
650 words / 3906 chars
Revision
REV-I
Version Released
v1.0

Docker Compose for homelab beginners

Why Compose, not just docker run

If you’ve only ever run containers one at a time with docker run, you already know the pain: remembering which flags you used last time, manually creating networks, starting things in the right order after a reboot. Docker Compose fixes all of that with one YAML file that describes your entire stack — and one command to bring it all up or tear it all down.

For a homelab, that’s the difference between “I’ll fix the server when I get home” and “I’ll SSH in and run docker compose up -d from my phone.”

The stack we’re building

A dead-simple, real-world shape: a reverse proxy in front of an app, talking to a database. This is the backbone of a huge number of self-hosted services (Nextcloud, Ghost, Immich, you name it).

A minimal homelab Compose stack

  • nginx — reverse proxy, the only thing exposed to the internet
  • app — your actual service
  • postgres — the database, never exposed outside the Docker network

The docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
version: "3.9"

services:
proxy:
image: nginx:1.25
ports:
- "443:443"
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- app
restart: unless-stopped

app:
image: yourapp:latest
environment:
DATABASE_URL: postgres://appuser:${DB_PASSWORD}@db:5432/appdb
depends_on:
- db
restart: unless-stopped

db:
image: postgres:16
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: appdb
volumes:
- db_data:/var/lib/postgresql/data
restart: unless-stopped

volumes:
db_data:

A few things worth calling out for beginners:

  • Only proxy publishes ports. app and db don’t need ports: at all — Compose automatically puts every service on the same internal network, so app can reach db just by using the service name db as the hostname. That’s why the connection string above says @db:5432, not an IP address.
  • depends_on controls start order, not readiness. Compose will start db before app, but it won’t wait for Postgres to actually finish initializing. For anything that matters, add a healthcheck (see below) or build in retry logic on the app side.
  • Named volumes (db_data) survive docker compose down. If you want to wipe everything including data, you need docker compose down -v — that extra -v is the difference between “restart clean” and “delete my database.”
  • ${DB_PASSWORD} pulls from a .env file sitting next to your docker-compose.yml. Never hardcode secrets directly in the YAML if you’re ever going to commit this to git.

Adding a healthcheck (the thing most tutorials skip)

1
2
3
4
5
6
7
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser"]
interval: 5s
timeout: 5s
retries: 5

Then change app’s dependency to actually wait for it:

1
2
3
4
app:
depends_on:
db:
condition: service_healthy

Now app won’t even attempt to start until Postgres reports healthy — no more crash-loop on first boot after a reboot.

The commands you’ll actually use

1
2
3
4
5
6
docker compose up -d        # start everything, detached
docker compose logs -f app # tail logs for one service
docker compose ps # see what's running and its health status
docker compose restart app # restart just one service
docker compose down # stop and remove containers (keeps volumes)
docker compose pull && docker compose up -d # update to latest images

Where to go from here

Once this clicks, the natural next steps are:

  • Put Traefik or Caddy in front instead of raw nginx for automatic HTTPS certificates
  • Split secrets into a proper .env file and add it to .gitignore
  • Add a second Compose file (docker-compose.override.yml) for dev-vs-prod differences
  • Look at Watchtower for automated image updates, if you trust upstream maintainers enough

Compose won’t replace Kubernetes for a real production fleet, but for a homelab running a dozen self-hosted services on one box, it’s honestly all you need.

§ 05 SECTION

Related Documents

Other specifications filed under the same module.

§ 06 SECTION

Field Notes & Comments

Session-local discussion — comments here are not persisted to a server in this build.

Loading discussion module…