After going back and forth between several self-hosting options, I ended up centralizing everything on a mini PC with Docker Compose. Each service runs in its own container, with a reverse proxy in front to handle HTTPS and subdomain routing.
The base docker-compose.yml
services:
proxy:
image: caddy:2
restart: unless-stopped
ports:
- '80:80'
- '443:443'
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
photos:
image: ghcr.io/immich-app/immich-server:release
restart: unless-stopped
depends_on:
- db
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD: changeme
volumes:
- db_data:/var/lib/postgresql/data
volumes:
caddy_data:
db_data:
Services currently running
| Service | Role | Internal port |
|---|---|---|
| Caddy | Reverse proxy + auto HTTPS | 80 / 443 |
| Immich | Photo backup | 2283 |
| Vaultwarden | Password manager | 80 |
| Uptime Kuma | Service monitoring | 3001 |
A few commands I use every day to manage the stack:
# start all services in the background
docker compose up -d
# follow the logs of a specific service
docker compose logs -f immich-server
# update an image, then recreate the container
docker compose pull vaultwarden
docker compose up -d vaultwarden
Warning: never expose a sensitive service (a database, an admin dashboard) directly to the internet without authentication or a reverse proxy in front of it. A forgotten
ports: "5432:5432"can be enough to expose an entire database.
Next steps
- Set up automatic backups of the volumes to external storage
- Add two-factor authentication to Vaultwarden
- Document the full restore procedure, so I don't discover it the day I need it
The homelab remains a constantly evolving project: every new service is a chance to rethink the organization of the stack a little.