Infrastructure / automation

HomeLab & automation

Self-hosted production platform on residential hardware: deploys, mail, analytics, monitoring, and the reliability work that keeps it up.

  • Docker Compose
  • Traefik
  • Cloudflare Tunnel
  • Gitea
  • FastAPI
  • systemd
  • Home Assistant
  • Tailscale
  • MinIO
  • Uptime Kuma
  • Umami
  • OpenClaw
  • MCP
  • ESPHome
Self-hosted production platform Public ingress through a tunnel rather than an open port. One proxy fronts two machines, so a service can change hosts without touching DNS. PUBLIC EDGE AND PRINTERS MEDIA, BUILDS AND API LAN, NO DNS CHANGE EDGE Cloudflare tunnel no WAN ports open Traefik one route file, two hosts Sites + PWAs prerendered MCP fleet · CRM · HA agents and printers Ember API Kotlin and Ktor Media stack Quick Sync transcode Routes both hosts Outside the host Service
Public traffic enters through a tunnel rather than an open port. Traefik is highlighted because it fronts both machines from one route file, which is how a public service changed hosts with no DNS change and no downtime.

Overview

A personal production platform on residential hardware, not a demo on SaaS. It began on a single 2012 laptop and, as of August 2026, spans two machines: that laptop still terminates the public edge and runs the printers, the websites, the CRM and the MCP fleet, while a second box added this month took over the media stack, Android builds and a public API. Around 60 containers on the first host and a growing set on the second. The constraint is the interesting part: consumer hardware makes every reliability decision explicit rather than something a datacentre absorbs.

Problem

Running real services from a home network means a dynamic IP, no port forwarding, residential mail rejection, and hardware that powers off. Cloud CI runners add cost and maintenance. But the harder problem is the one that only shows up after months of uptime: on constrained hardware, unrelated services quietly compete for memory, boot-time protections silently fail to persist, and alerting paths break without ever changing state, so the system looks healthy right up until it is not.

Solution

A tunnel for public ingress with no WAN ports open. GitHub stays the source of truth; Gitea mirrors repositories and webhook-driven scripts build in throwaway containers and publish static sites, serialized behind a lock so two pushes cannot race. Postfix relays through a transactional provider for deliverability. systemd units own boot order, a hardware watchdog, and an automated reboot that asks the printers for permission first. Health checks understand printer power state, so a machine that is deliberately off does not page anyone.

Architecture

The first host runs Traefik with per-service Docker label routing, nginx for static sites, Gitea with self-hosted Actions runners, the contact API, Postfix, Home Assistant with ESPHome devices, Uptime Kuma, self-hosted Umami analytics, a self-hosted CRM, three MCP servers, an agentic Telegram gateway, a Firefly III ledger, the Jove print orchestrator and two Klipper printers. The second host runs the media stack with hardware transcoding, the Android build toolchain, and a public API. A third site in another building adds six printers over a private mesh. A push to master triggers mirror sync, a build in a throwaway container, and a publish to the static site root; portfolio deploys also rebuild the API image.

Key decisions

A tunnel over port forwarding, so nothing is exposed by an open port, and pointed at the proxy rather than at any individual service. That indirection is what let a public API move to a different machine with no tunnel change, no DNS change and no downtime: the proxy grew one route entry pointing at the other host over the LAN, which container labels cannot express because they only describe local containers. Per-service Docker labels for everything that does stay local. Self-hosted CI over cloud runners. Builds in ephemeral containers, because the hosts deliberately carry no Node or Python toolchain. Isolated compose projects per service, since a shared project name makes a routine teardown command destructive to unrelated stacks. Latency-critical processes pinned out of swap rather than swap disabled, because removing swap outright converts a stall into an out-of-memory kill.

Challenges

The instructive failures were all silent. Two deploys landing seconds apart let one delete the working tree out from under the other, and it surfaced as a build error naming a file that plainly existed, which sends you debugging the application instead of the pipeline. A hardware watchdog looked correctly configured and had not survived a single reboot, because the distribution deny-lists that module in a per-kernel file and the check had only ever been run in the same session as the fix. A monitoring dashboard reported every public site down while all of them were up, because the checks ran from inside the same network and could not reach the edge addresses serving its own zone. Each one taught the same lesson: verify a persistence change after a reboot, and verify a failure path from outside the system that owns it.

Outcome

A live portfolio and API on theevolvedalligator.com, automated deploys on every push, a box that self-recovers from the class of hang that used to take it down for hours, and a second machine absorbed without a single public URL changing. More to the point, it is where the operational instincts come from: the habit of asking how a system fails quietly, of distrusting a green check that was never tested against a restart, and of writing the reasoning down where the next person can find it.

Wondering if Daniel is a fit? Ask his assistant, hiring, contracts, or collaboration.