Contents

Technical Debt in a Home Lab: When to Refactor and When to Let It Rot

Not every duct-taped service deserves to be rebuilt properly

Contents

Somewhere in my homelab there is a service running in a container I built by hand in 2021, configured by SSH-ing in and editing files directly, with no Compose file, no documentation, and a startup command that lives only in my shell history. It has not gone down once. I am terrified of it, and I have decided to leave it exactly where it is.

This is the dirty secret of running infrastructure for fun: technical debt is real here too, but the economics are completely different from a workplace. At work, debt compounds across a team and a roadmap. At home, it compounds across exactly one person — you — and the only deadline is your own patience. That changes the calculus enormously, and it means the professional instinct to “do it properly” is sometimes the wrong instinct.

What technical debt looks like at home

Advertisement

In a homelab, debt doesn’t show up as a sprint-planning ticket. It shows up as friction. The signs are familiar once you name them:

  • A service only you know how to restart, because the steps live in your head
  • Configuration edited in place, with no record of why it’s set that way
  • Pinned-but-ancient images you daren’t update in case they break
  • “Temporary” hacks — a hardcoded IP, a disabled TLS check — that have outlived several governments
  • That one box doing six unrelated jobs because migrating them sounds tedious

None of this is inherently a crisis. The question is never “is this debt?” — of course it is. The question is whether it is costing you anything.

The trap I fell into for years was importing the workplace framing wholesale. At work, a code reviewer flags the hardcoded value, the sprint has a “tech debt” column, and there’s a shared understanding that debt slows the team down, so paying it off is a collective good. None of that transfers home. There is no team. There is no reviewer. There is no roadmap that the debt is silently sabotaging. There is only you, a finite number of evenings, and a system whose only real job is to be useful and occasionally fun. Applying enterprise refactoring instincts to that is like power-washing your own driveway to a professional standard — technically admirable, mostly a waste of a Saturday.

There’s a related failure mode worth naming: debt you create by upgrading things that were fine. Every rebuild, every “let me just move this to the new stack”, introduces fresh unknowns into a system that had none. I’ve written a whole piece on the home-lab upgrade trap because it’s so common — the itch to modernise a working service is itself a source of debt, not a way of paying it down.

The only question that matters: does it hurt?

Professional refactoring advice obsesses over cleanliness. At home, cleanliness is a luxury, not a virtue. The single useful test I apply is this:

Is this debt charging me interest — in downtime, in anxiety, or in blocked progress — or is it just sitting there quietly being ugly?

Ugly-but-stable is fine. A homelab is not a portfolio piece. If a service has run untouched for two years and you never think about it, the debt has effectively been paid off by the simple fact that nothing changes. Refactoring it would mean introducing new bugs into something that currently has none, purely to satisfy your sense of tidiness. That’s not engineering, that’s redecorating.

When to actually refactor

Advertisement

There are clear triggers where the interest payments become real and you should bite the bullet:

  • It breaks repeatedly. A service you restart monthly is taxing you. That recurring cost justifies a rebuild.
  • You’re afraid to touch it. Fear is a symptom of missing reproducibility. If you can’t update it without holding your breath, the debt is now blocking other work.
  • The bus factor is bothering you. If a partner or housemate depends on it and only you can fix it, that’s a humane reason to make it boring and documented.
  • It’s the dependency under everything. Debt in your DNS, reverse proxy, or backup system is load-bearing. Pay that down first, always.

If several services are shouting at once, resist the urge to declare a grand “cleanup weekend” — those collapse under their own ambition. Rank ruthlessly instead. The load-bearing dependency comes first, always, because its failure cascades: debt in your DNS or reverse proxy doesn’t cost you one service, it costs you every service behind it. Next comes whatever charges the most frequent interest — the thing you restart weekly beats the thing that scared you once a year ago. The genuinely-scary-but-stable container comes near the bottom, because fear is not the same as cost, and something you’re afraid of but never actually touch is charging you nothing but a feeling. Sort by interest paid, not by how ugly it looks or how much it nags at your pride.

The fix is usually less heroic than you fear: capture the running state into a Compose file so the box stops being a pet.

1
2
3
4
5
6
7
8
# the migration from "it lives in my head" to "it lives in git"
services:
  oldthing:
    image: someimage:1.4.2   # pin it, then you can move it deliberately
    restart: unless-stopped
    volumes:
      - ./data:/data         # the bit you actually care about
    env_file: .env           # secrets out of the command line

Commit that, and the unknowable hand-built container becomes a thing you can rebuild on any machine in thirty seconds. That alone retires most of the fear.

There’s a natural next step here that I resisted for a long time and now swear by: put the boring, repeatable part of the rebuild into a task runner so “how do I bring this back” stops living in your head. I use a Taskfile for exactly this — the recreate-from-git dance becomes task restore, self-documenting and greppable, which is the whole argument in my Taskfile writeup. The point isn’t the tool; it’s that the recovery procedure becomes an artefact instead of a memory.

A worked example of paying interest

The last service I actually rebuilt wasn’t the scary 2021 container — it was my reverse proxy, and the trigger was textbook. It had started failing its certificate renewals intermittently, which meant I was manually poking it every few weeks. That’s the “it breaks repeatedly” signal, and because everything else fronts through it, it was also load-bearing. Two triggers, both real. The rebuild took an evening: I captured it into a Compose file, pinned the image, moved its config into version control, and wired renewal into a health-checked routine. The interest payments stopped immediately. I’d been putting it off for months because it felt like a big job; the actual work was small, and the relief was disproportionate. That’s usually how load-bearing debt goes — the dread is bigger than the task.

When to deliberately let it rot

And here is the heresy. Sometimes the correct, mature, senior-engineer decision is to do nothing at all. Let it rot if:

  • It works, you don’t touch it, and nothing depends on it being modern.
  • It’s scheduled for replacement anyway — don’t polish what you’re about to demolish.
  • The data is backed up. As long as you can resurrect the state, the messy plumbing around it is disposable.

That last point is the key that lets me sleep. I don’t need my scary 2021 container to be well-engineered. I need its data backed up and a rough idea of how to recreate it. Given that, the container itself can be as ugly as it likes. Rot is only dangerous when it can take your data with it.

When the “let it rot” call goes wrong

Deliberate rot is a strategy, not an excuse, and it fails in two predictable ways worth watching for.

The first is silent dependency creep. You decided a service was safe to ignore because nothing depended on it — and then, six months later, you quietly wired something new on top of it without re-running the assessment. The classic version is an ancient database that started as a toy and is now, without anyone deciding this, holding data three other services rely on. The fix is a periodic honesty check: every so often, ask of each rotting service, “what breaks if this vanishes tonight?” If the answer has grown since last time, the rot decision has expired and needs remaking.

The second is the un-updatable security surface. Letting the plumbing rot is fine; letting an internet-exposed, unpatched service rot is not. If a rotting box is reachable from outside your network — or runs something with a history of remote exploits — “it works, don’t touch it” turns into “it works, and so does the botnet living on it”. Rot is a luxury reserved for things that are internal, well-firewalled, and backed up. Anything on the edge gets patched or gets retired; there is no third option.

The tell that you’ve misjudged a rot call is always the same: you find yourself thinking about the service. The whole justification for letting something rot is that it has faded from your attention. The day it re-enters your thoughts — because it hiccuped, because you got nervous about a CVE, because something new leans on it — is the day the calculus changed and you owe it a fresh decision.

The honest verdict

In a homelab, technical debt is not a moral failing and “do it properly” is not a commandment. Spend your limited evenings paying down the debt that actually charges interest — the flaky service, the load-bearing dependency, the thing only you can fix — and let the quiet, ugly, stable corners rot in peace.

The goal of a homelab is to be useful and to be fun. Refactoring something that is already both, purely because it offends your tidiness, achieves neither. Back up your data, fix what hurts, and learn to walk past the rest without flinching.

If I’ve made this sound like an excuse to be lazy, I’ve said it wrong. Deliberate rot is more demanding than reflexive tidying, not less, because it asks you to hold two uncomfortable things at once: an honest map of what actually depends on what, and the discipline to leave working things alone even when your hands itch to improve them. The tidy instinct is easy; it just wants everything uniform. The mature call is harder — it looks at a specific ugly service, asks what it’s genuinely costing, and answers with money and evenings rather than aesthetics. Get that judgement right consistently and your lab stays both reliable and enjoyable, which, again, was the whole point. Get it wrong in the tidy direction and you’ll spend every weekend rebuilding things that were fine; get it wrong in the lazy direction and one rotting dependency eventually takes your data with it. The skill is living in the narrow band between those two failures.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.