使用 Compose、Ruby、IPv6 且无需 Kubernetes 重建我的家庭实验室
Rebuilding My Homelab with Compose, Ruby, IPv6, and No Kubernetes

原始链接: https://www.petekeen.net/homelab-resolved/

在经历了硬件不稳定和 Kubernetes 带来的过度复杂性后,作者对自己的家庭实验室进行了彻底改造,转向了一套更易于管理的定制系统。他们放弃了单体集群架构,改用由“激进(spicy)”的 Docker Compose 堆栈驱动的可靠模块化方案。 当前的架构依赖于几台关键设备:一台用于关键服务的稳定 Proxmox 主机、一台专用的 TrueNAS 存储服务器,以及一台性能强大的、负责处理其他所有任务的 Ryzen 主机。该系统通过自定义 Ruby 脚本和 YAML 配置文件进行管理,能够生成统一的部署计划。 该系统的一个显著特点是采用了确定性的 IPv6 寻址,这使得不同主机上的服务能够在无需复杂网络覆盖的情况下进行安全通信。通过摒弃 Kubernetes 的开销,作者创建了一个可预测、易于排查且完全由自己掌控的系统。这种“手工打造”的方法将家庭实验室视为一组隔离的故障域,确保小故障不会导致整个系统瘫痪。对于那些更喜欢定制化自动化而非现成编排工具的人来说,这是一种务实且充满乐趣的替代方案。

在最近的一场 Hacker News 讨论中,开发者 Pete Keen 分享了他重建个人家庭实验室的经历,他选择放弃 Kubernetes,转而采用由 Docker Compose 和 Ruby 组成的更简单技术栈。 Keen 解释说,他的目标是创建一个易于维护的系统,无需投入 Kubernetes 管理所需的持续成本。他强调,在新的配置中,使用确定性的 IPv6 地址进行容器接入是一项关键创新。 这篇文章引发了一场关于编排工具复杂性的简短讨论。虽然一些评论者指出 Kubernetes 为大规模环境提供了自动扩展和故障转移等必要功能,但另一些人则认为对于个人项目而言,它往往是大材小用。Keen 本人澄清说,他的决定是出于作为独立维护者的现实局限性,而非对 Kubernetes 持反对态度。这场对话最终探讨了轻量级“胶水代码”架构与意外从零开始重构出一个复杂编排器之间的界限。
相关文章

原文

A little less than a year ago I wrote:

Everything is in shambles.

One machine is dead and probably not coming back. Services are randomly scattered amongst the survivors. My Kubernetes project is kinda-sorta paused.

This post is an addendum to that one, where I'm going to give an overview of what I've settled on, as much as one can settle in a homelab.


To recap a bit, in the previous post I had a bunch of machines that were mashed together into something resembling a coherent cluster:

  • hypnotoad, crushinator, and a precarious control plane VM running on a thin client
  • lrrr, an N150 box running Proxmox
  • nibbler, a piece of shit

Of those four machines, lrrr is the sole survivor.

Nibbler, on television, is an adorable little three-eyed dude in Futurama that (courtesy spoiler warning, but 20 years is well past my cutoff) turns out to be a member of a hyperintelligent race of beings that can time travel and who, in fact, is the reason why Fry fell into the cryopod to start with.

nibbler, in my basement, was a machine that I had pinned a lot of hope on. It was a Lenovo M80s Gen3 SFF with an i7-12700 and DDR5 memory, picked up on eBay for what seemed like a steal (foreshadowing). I upgraded the memory to 128GB and the storage to 3TB of NVMe with left over pocket change. I put an HBA in it and hung a disk shelf off of it with seven 16TB drives. It was to be the core server, the one where all the action would happen.

It was, as covered earlier, a piece of shit.

I'm still not quite sure what was wrong, but it was never reliable. Often it would just not come up after a reboot. Once in a while it would just forget about hardware.

On more than one occasion I or my spouse would discover that Jellyfin wouldn't play anything anymore, and I would shuffle down to the basement to attach a monitor and keyboard and discover that the entire storage disk shelf was errored out with inscrutable SAS adapter driver errors in the kernel logs.

Around the time I wrote the previous piece I put together a likely story. The eBay seller knew this machine was bad but they slapped an "eBay Certified Refurbished" label on anyway and sold it for a song to a sucker. By the time I realized this I had modified the machine so extensively I couldn't make a refund claim.

Simultaneous to the nibbler revelations I had another realization: Kubernetes is Too Hard. I built a system that I didn't actually know how to maintain without the time or energy necessary to dig myself out of trouble. When the machine that has more cores and memory than every other machine in the cluster combined decides to self-immolate once or twice a week it puts a huge damper on the whole experiment.

My stop-gap solution was to revert everything important back to the way things were before k8s, which was effectively just docker compose. After letting that bake for a while I decided to just roll with it.

It's now July of 2026.

lrrr exists in much the same form as it did before. It's still running Proxmox, but the only virtual things there are one LXC running Omada and one tiny VM that I've been using to ease myself into the agentic lifestyle. I've also done the worst thing imaginable to the Proxmox forum trolls: I've installed Docker on the host to run core services like Home Assistant and friends.

hypnotoad and crushinator are turned off, lying in state until I need them again.

morbo is a new-to-me HP Elitedesk 800 G3 SFF that runs TrueNAS SCALE and whose entire job is to manage storage and stay out of the way and it does a great job. The end.

ord-router is a VM running at a datacenter in Chicago that happens to have direct peering with Comcast. This is the public ingress node.

nibbler is now an amalgamation of old and new parts: same memory, same storage, new gamer-style case and motherboard and processor (Ryzen 9 9900X). It also recently gained an RTX 5060ti 16GB video card for local LLM experiments. This is the "everything else" machine. Everything that isn't critical to the house functioning as a house and the network as a network runs here.

Among other things, nibbler is where Forgejo, Minecraft, and Jellyfin and friends live, along with a bunch of other random crap.

There are also a handful of Raspberry Pi-class devices scattered around that are also deployed with this system, but they do appliance-type things: house-pi runs a Z-Wave stick and shed-pi is where the ADS-B receivers are plugged in.

Quirky? Yeah, let's go with quirky.

The software stack that deploys the homelab is arranged like this:

  • a bunch of spicy docker compose stacks
  • a handful of Ruby libraries that implement the spice
  • a Rakefile to drive the libraries
  • a YAML file that ties it all together

What's the spice, you ask?

  • cron jobs
  • secrets management
  • secure defaults
  • http ingress via Caddy with optional authn/authz and optional public-facing IP
  • automatic certificate management via Lego
  • backups
  • monitoring
  • dashboard
  • static websites served by the ingress
  • cross-stack dependency declarations
  • cross-platform docker image builds

Each compose stack declares what it wants using compose extensions (i.e. fields that start with x-). Here's a simple example:

services:
  metube:
    image: ghcr.io/alexta69/metube
    container_name: metube
    hostname: metube
    restart: unless-stopped
    x-security:
      enabled: true
    volumes:
      - media-nfs:/media
    environment:
      DOWNLOAD_DIR: /media/youtube
      HOST: '::'
    x-backup:
      enabled: false
    x-web:
      port: 8081
      auth: true
      dashboard:
        name: MeTube
        subtitle: Download YouTube Videos
        category: Media and Games

x-depends:
  - media-volume
      

This sets up MeTube, a little app that grabs videos from various places around the internet. It enables the secure defaults, it declares that it doesn't need backups, and then it sets up ingress. Here, x-web specifies the port, that it wants authentication/authorization, and that it should show up on the dashboard.

This example also illustrates the cross-stack dependencies. It pulls in the media-volume stack which sets up the media-nfs volume.

The trick that lets all that work is that all of the stacks get smushed into one big docker-compose file at deploy time. That means I don't have to do gross things like declare external volumes or networks, it all just hangs together.

In a normal docker compose world every container in a stack shares a bridge network. For my purposes that doesn't make sense. I don't want some random vibe-coded thing that I downloaded yesterday to be able to talk directly to lldap, for example.

Instead, I emulate what you'd get if you were deploying individual stacks: each stack gets a bridge network to itself, and containers in other stacks can join it when necessary.

The natural way to implement ingress, then, would be to have every container that serves a website join a shared ingress bridge. Again, though, that doesn't really fit what I'm going for. I want to preserve the separation between stacks so that an extruded todo list manager that gets popped doesn't threaten everything else.

My solution is deterministic IPv6:

  • The fleet as a whole gets a /48 IPv6 ULA, which is similar in concept to an RFC1918 IPv4 network (ex: 10.10.10.0/24) but drawn from a vastly larger pool
  • Each host gets a /64 by hashing the hostname + salt for the next 16 bits
  • Each stack gets a /96 by hashing the stack name + salt for the next 32 bits
  • Each service gets a /128 by hashing the container name + salt for the last 32 bits

In other words:

<48 bit ULA prefix>:<16 bit host>:<32 bit stack>:<32 bit service>

Caddy runs with network_mode: host and all of the generated upstreams point at the IPv6 assigned to the containers, which works because the host naturally has access to all of the bridges.

Another weird/brave/stupid thing about my setup is that lrrr and nibbler both have ports 80 and 443 exposed to the internet over IPv6. Caddy is set up to reject non-local traffic unless the service specifically opts in with a public: true route. This is mostly for convenience so I don't have to set up Tailscale on mobile devices to access things like Jellyfin and the dashboard.

Oh, and Tailscale. Tailscale is so cool. Except on my iPhone which it makes very hot and drains the battery.

Tailscale is the backhaul for a few important things. Web traffic that hits ord-router is routed to the hosts in the basement over Tailscale. It's also how Caddy on each host talks to the central authn/authz system running on lrrr.

First, that I have not built a Kubernetes. Nothing about this changes at runtime so I don't need etcd or Raft or consensus of any sort. Everything is statically determined up front, with linters and tests and other nice things.

Second, the failure domains concept actually works really well in practice. lrrr is home for critical stuff which is kept to a minimum. nibbler is everything else that isn't a weird one-off appliance.

If nibbler goes down people are sad but not mad. If lrrr goes down the HVAC system gets a little dumber and the LEDs we rely on to tell us the cats are outside stop working.

Finally, it's really nice to have full control over the system. For example, the other day I decided to implement that static sites via ingress thing. I didn't have to dig for a solution on some kubernetes discourse that is five years out of date and then translate it forward. I just wrote some code and made it happen. Same with the IPv6 thing. Being able to do that smoothly is a consequence of having complete control.

Oh, and also, if none of this sounds like type one fun for you then sticking with Kubernetes or one of the zillion docker compose managers that people have put together is completely valid. I see you. It's ok.

I have a sanitized public snapshot of the code that builds the whole thing on my Forgejo if you're interested.

Wanna chat about this kind of thing? Links to my socials etc are in the footer.


It's 2026 so I guess this needs to be explicit: these are artisanal, human-produced words. The machines suggested fixes for my hackneyed clichés but I didn't use any of their words.

联系我们 contact @ memedata.com