One box, 19 self-hosted services, no port forwarding: remote access is Tailscale only.
This is the full config for the Linux box under my desk (hostname arroz, Portuguese for rice). It runs the stuff my family and I actually use every day: passwords, shopping lists, fitness tracking, location history, Spotify stats, a retro game library. It also runs a couple of things I wrote myself.
Every service is a small Docker Compose stack in its own folder. They all share one homelab network behind a single Caddy reverse proxy. Nothing is exposed to the public internet. At home I reach everything through local DNS names, and away from home I go through Tailscale.
- Two ways in, one proxy. Plain-HTTP
*.homelabnames for the LAN, resolved by AdGuard. Services that need HTTPS (Vaultwarden, the PWAs, OAuth callbacks) get their own port on the Tailscale hostname, using a real cert fromtailscale cert. - Databases stay private. Postgres, MariaDB, Mongo and Redis only live on per-stack internal networks. Only the frontends join
homelab. - Ops: Homepage is the dashboard, Komodo manages the stacks, WUD checks for image updates every morning, and Netdata watches the host.
| Service | What it does here | |
|---|---|---|
| Infra | AdGuard Home | Network-wide DNS, ad blocking and the *.homelab names |
| Caddy | The one reverse proxy in front of everything | |
| Homepage | Dashboard with live widgets for every service | |
| Komodo | Web UI to deploy and manage the compose stacks | |
| What's Up Docker | Tells me when an image has a new version | |
| Netdata | Real-time host and container metrics | |
| Family | Vaultwarden | Bitwarden-compatible password manager |
| KitchenOwl | Shared grocery list and recipes | |
| Dawarich | Self-hosted Google Timeline replacement | |
| SparkyFitness | Nutrition and workout tracking | |
| Home Assistant | Home automation | |
| Productivity | ConvertX | Convert any file to any format |
| Stirling PDF | Every PDF tool you'd ever need | |
| Excalidraw | Whiteboard and diagrams | |
| Obsidian LiveSync | CouchDB backend syncing my Obsidian vault across devices | |
| PR Review ⭐ | Custom: static-analysis bot for my GitHub PRs | |
| Fun | Your Spotify | Spotify Wrapped, but all year round |
| RomM | Retro game library that plays in the browser | |
| insta-bot ⭐ | Custom: answers League of Legends invites in the group chat |
🔍 prreview/: self-hosted PR reviewer
A Python service that polls GitHub for open PRs and mirrors each repo. It runs ruff, bandit, semgrep plus a raw syntax check (YAML/JSON/XML) on the files the PR changes, separates findings on the lines it actually touched from the rest, and publishes an HTML report at prreview.homelab. It remembers the head SHA of every PR, so each push is analysed exactly once. The GitHub token goes through GIT_CONFIG_* env vars, never argv or a stored remote URL, so it never shows up in ps.
🎮 insta-bot/: "bora" as a service
My friends invite each other to play LoL in an Instagram group chat, and this bot answers for me. It drives a real headful Chrome under Xvfb with Playwright and watches the thread with a DOM MutationObserver. When someone sends an invite ("siga jogar?", "alguém rankeds?", "flex?") it replies with a random "bora", with a global cooldown and accent-insensitive wildcard triggers. It runs as a systemd timer during evening hours only, and a League-themed control panel behind Caddy basic auth handles on/off, dry-run, schedule, triggers and replies, all live without a restart.
💾 scripts/backup.sh: consistent backups the dumb way
It stops every container, tars the homelab folder, the Docker volumes and the certs from a throwaway Alpine container, then starts everything again. About 60 seconds of downtime buys a crash-consistent copy of every Postgres, Mongo and SQLite database, with zero per-service dump logic. It also warns about any new bind mount outside the backed-up paths.
Nothing sensitive lives in this repo:
- Every stack reads its credentials from a local
.env, which is gitignored. Each one has a committed.env.examplethat lists the keys it needs. - Machine-specific values (my tailnet hostname and Tailscale IP) are env vars too:
${TS_HOST}in compose,{$TS_HOST}in the Caddyfile,{{HOMEPAGE_VAR_TS_HOST}}in Homepage. - App data (
data/, databases, Home Assistant config, logs) is gitignored.
.
├── caddy/ Caddyfile + reverse proxy stack
├── <service>/ docker-compose.yml, .env.example, config (if any)
├── homepage/config/ dashboard: services, widgets, theme
├── prreview/ custom PR reviewer (Dockerfile + Python)
├── insta-bot/ custom Instagram bot (Python + systemd units + web panel)
└── scripts/ backup
docker network create homelab # once
cd vaultwarden
cp .env.example .env && $EDITOR .env
docker compose up -dFor the Tailscale HTTPS ports, put the cert from tailscale cert <host> in /etc/caddy/certs/ and set TS_HOST in caddy/.env.