Skip to main content

Command Palette

Search for a command to run...

How I Stopped Drowning in Incus Containers (and Started Loving My Homelab Again)

Updated
4 min read
A

I'm a developer from Jakarta, Indonesia. An average one, or perhaps little bit mediocre. Likes to learn new thing. Currently learning Rust, Vlang, C++17/20, and some frontend stuffs.

A few months ago, I had a problem:

I couldn’t remember what half my containers were for. Seriously.

I’d run incus list and see:

+-----------------+---------+----------------------+-----------+-----------+
|      NAME       |  STATE  |       IPV4           |   TYPE    | SNAPSHOTS |
+-----------------+---------+----------------------+-----------+-----------+
| web01           | RUNNING | 10.209.83.196 (eth0) | CONTAINER | 0         |
| api-dev         | RUNNING | 10.209.83.177 (eth0) | CONTAINER | 0         |
| tile-cache      | RUNNING | 10.209.83.201 (eth0) | CONTAINER | 0         |
| db-geo          | RUNNING | 10.209.83.155 (eth0) | CONTAINER | 0         |
| temp-test       | STOPPED |                      | CONTAINER | 0         |
| legacy-app      | STOPPED |                      | CONTAINER | 0         |
        ...           ...             ...               ... 
+-----------------+---------+----------------------+-----------+-----------+

And I’d stare at it like…
- “Wait — does api-dev use db-geo? Or was that web01?”
- “Is tile-cache still needed? Who uses it?”
- “Why is there a temp-test from six weeks ago??”

My lab had become a container graveyard — full of forgotten services, scattered docs, and tribal knowledge only I (barely) remembered.

The Breaking Point

One day, I cloned a container for a new project, started it up… and got this SSH warning:

WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

Because — duh — it inherited the same SSH host keys as the original. And that was it. I’d had enough.

I wanted:

  • To know what each container was for

  • To not break things by accident

  • To spin up full environments with one command

  • To stop guessing

So I built LabKit.

What Is labkit?

labkit is not another orchestration tool. It’s not Kubernetes. It’s not even trying to be.

It’s a lightweight, Git-powered wrapper around Incus that helps you treat your containers like actual projects — not just random Linux boxes.

Think of it like create-react-app, but for full development environments.

With labkit, I now do this:

labkit new backend-api
cd backend-api
labkit node add postgres
labkit node add api-server
labkit requires add tile-server
labkit up

And boom — my whole stack is up, documented, and connected.

How It Works (Without the Boring Parts)

Every lab is just a folder with:

backend-api/
├── lab.yaml               # config: template, deps
├── nodes/
│   ├── postgres/          # auto-created
│   │   ├── manifest.yaml  # "This is a PostGIS DB"
│   │   └── README.md      # Usage notes, queries, tips
│   └── api-server/
│       ├── manifest.yaml
│       └── README.md
└── shared/                # scripts, configs, certs

When I run labkit up:

  • It starts postgres and api-server

  • But also checks if tile-server (a shared map tile server) is running — and starts it if needed

  • All docs are mounted into the containers at /lab/node so anyone can read them

  • And if I ever forget what api-server does? I just cat /lab/node/README.md

No more Slack pings. No more digging through notes.

The Magic: Shared Infrastructure That Doesn’t Get Killed

Here’s my favorite part:

I have a shared OSM tile server used by three different labs.

Before labkit, if I ran incus delete --all, I’d accidentally kill it.

Now, I tag it:

incus config set tile-server user.pinned=true
incus config set tile-server user.required_by=backend-api,frontend-map,analytics-dashboard

And when I run labkit down in any lab?

  • It stops local containers

  • Leaves tile-server alone

  • Even if it’s not “in” that lab

It just knows: “Someone else needs this.”

Bonus: Start Only What You Need

Sometimes I don’t want the whole stack.

So I added:

labkit up --only api-server

Perfect for testing one service without bringing up five others.

And yes — --dry-run works everywhere:

labkit down --only postgres --dry-run

Output:

Planned actions:
  Stop local node: postgres
DRY RUN: No changes applied

Peace of mind before pressing “go”.

Why This Matters

labkit didn’t just organize my containers. It changed how I work.

Now:

  • New projects take minutes, not hours

  • I never lose context

  • I can safely clean up old stuff because I know what depends on what

  • Documentation lives where it’s used — inside the environment

  • And yes, no more SSH host key warnings 😌

It’s not fancy. It doesn’t scale to 1000 nodes.

But for real developers managing real homelabs, it’s exactly what we need.

Want to Try It?

Check out github.com/aprksy/labkit or the project page https://labkit.aprksy.dev

Then:

labkit new myproject
labkit node add web
labkit up

That’s it.

No YAML sprawl. No complex CRDs. Just containers that make sense.


Final Thought

We spend so much time building apps that we forget to build good environments for building them.

labkit is my way of fixing that. And honestly? I finally enjoy using my homelab again.

No more chaos. Just clarity.

I guess I will write another post on how I setup my homelabs in more detailed.