Software Engineer
Loading posts...
The living reference for my homelab: six hosts, ~100 containers, SSO on everything, DNS as code, borg backups, and the honest cost table at ~$64/mo.

What started as a 'let me try rendering OpenStreetMap data' turned into a full-blown parallel map rendering engine. Here's how I built a system that processes millions of geographic features and renders beautiful maps with airports (because I love planes).

A deep dive into the interactive easter eggs we secretly embedded throughout the HackPSU Fall 2025 website – from memory games to fish that chase your cursor.
Feel free to contact me at kanishksachdev@gmail.com
The complete service map: every host, every container, and the wiring between them. The companion post, Homelab: Current Architecture, is the overview with the incident stories and the cost table; this one is the deep dive. Refreshed August 2026: six hosts and about a hundred containers, up from the three VPSes this post originally described.
All six are docker-compose hosts deployed the same way: git push from the
Mac, then an Ansible playbook that pulls on each host, renders secrets from
Infisical, and runs compose up.
Loading diagram…
The compose split matters operationally: compose.media.yml,
compose.observability.yml, compose.mail.yml and a dozen more are separate
files but one compose project, so a deploy reconciles everything while edits
stay scoped to one stack. Authentik's server and worker live in a second
compose project on the same host, because its database migrations need to
finish before the outposts restart; the split enforces the ordering.
Loading diagram…
The socket proxies are locked down: read-only endpoints, writes disabled at
the proxy, and published exclusively on each host's Tailscale IP. That last
part is the one worth auditing: a socket proxy bound to 0.0.0.0 is an
unauthenticated container inventory for whoever finds it, and provider
firewalls are not something I want to depend on.
Everything permanent in Authentik lives in 25 blueprint YAML files in git: 17 providers, their applications, group policy bindings, and the login, recovery, and invitation-enrollment flows.
Loading diagram…
The access matrix, by auth style:
| Apps | Style | Gate |
|---|---|---|
| traefik dashboard, crowdsec UI, cloudcmd, houndarr | proxy forward-auth | admins group |
| sonarr, radarr, bazarr, prowlarr | proxy forward-auth | arr group, with API-path bypass routers so each app's own API-key auth keeps working for automation |
| transmission (remote host) | proxy forward-auth | its own group |
| jellyfin, grafana, portainer, karakeep, sure, wardrowbe, technitium | native OIDC | per-app policy |
| kanishk-desktop | RAC (RDP in browser) | admins group |
| jellyseerr, infisical, actual, pocketbase, ntfy, roundcube | app-native auth | n/a |
| status page, public photo gallery | none, deliberately | n/a |
Forward-auth is the default for admin UIs because it needs nothing from the app. OIDC is used where the app keeps real user records (jellyfin watch state, grafana dashboards), so identity flows into the app instead of stopping at the proxy. Two of the OIDC apps link SSO logins to existing local accounts by email, which is safe only because Authentik owns and verifies the email claim; both required an explicit opt-in flag, and without it each would have silently created a second empty account on first SSO login.
Fourteen playbooks, five roles, six targets, zero agents. Plain SSH over Tailscale from the Mac.
| Playbook | Job |
|---|---|
deploy.yml | The daily driver: pull, render secrets, compose up, prune images if HEAD moved |
status.yml | Fleet health summary |
ping.yml / discover.yml | Liveness and read-only fact gathering |
bootstrap.yml | Fresh VPS to deploy-ready, idempotently |
restart.yml | Recreate one service or all of them on a host |
backup.yml | Install or refresh the borg systemd units |
bitmagnet-prune.yml | Install the DHT-crawler retention timer |
crowdsec-ssh-bypass.yml | Install the firewall-bouncer SSH guard |
secrets*.yml | Push secrets into Infisical, export backups out |
| Role | Purpose |
|---|---|
host-bootstrap | Docker install, group membership, per-host deploy-key SSH config |
infisical-env | Pull prefix-scoped secrets via Universal Auth, render the host's .env and any single-file secrets, with per-file ownership |
hetzner-backup | Borg backup + integrity-check units and timers |
hetzner-crowdsec | The host-level nftables bouncer |
hetzner-secrets-backup | Periodic encrypted export of Infisical state |
Conventions that hold everywhere: no global become (tasks escalate only
when they need to), idempotency as a hard rule, per-host read-only deploy
keys so one compromised box can read only its own config, and host-key
checking off because Tailscale already authenticates the network.
Loading diagram…
Loki's pattern detection is on: it clusters the incoming stream into templates, which turns ten thousand near-identical lines into a dozen shapes with placeholders. Log coverage is hetzner plus three remote hosts; the remaining two are on the list. Prometheus also ingests dead-man's-switch metrics from cron-style jobs. The bitmagnet prune timer, for one, alerts if it stops reporting or if its delete count spikes past a day's intake, either of which means an upstream layer broke.
Loading diagram…
Every container uses named volumes, never bind mounts for state. State stays out of the git checkouts, which are also home directories on half the fleet. The tradeoff is that a handful of things are volume-state only and exist in no repo: the mail server's account config, Home Assistant's storage-backed settings, the status page's monitor definitions. The private runbook tracks exactly which volumes those are, because they're the ones a restore actually has to get right.
This post first described three VPSes and 30 containers. Since then: the
fleet doubled to six hosts by absorbing three machines at home; Coolify came
and went; the update-notifier dashboard was replaced outright by self-hosted
Renovate opening pinned-digest PRs across all nine repos; distributed
transcoding was retired as not worth its disk; DNS moved from dashboard
clicks to DNSControl with CI drift detection; CrowdSec grew from one bouncer
to two plus log-driven scenarios and an out-of-band WAF; secrets moved from
committed .env files (yes, really) to Infisical with gitleaks now standing
guard in every repo. The through-line: fewer moving parts I didn't build,
more of the boring machinery (deploys, secrets, backups, alerts) made
boring on purpose.