Karapace: because 'it works on my machine' is not an identity

I’ve been mass-producing side projects for a while now. Audio plugins, governance platforms, KDE contributions — the kind of portfolio that makes your Git profile look like a small software company run by one person with questionable time management.

But there’s a pattern that kept showing up across all of them, and it’s not a fun one:

Dev environments are fragile, implicit, and unreproducible.

You set something up. It works. You come back three weeks later, or you move to a different machine, or a colleague tries to build the same thing — and suddenly it doesn’t work. Not because anything changed on purpose, but because the environment was never really defined. It was just a pile of things that happened to be installed, in the right versions, at the right time.

🔗The itch

Immutable Linux systems are becoming the norm in a lot of places. The base OS is predictable, updates are atomic, and you don’t wake up to a broken desktop because some package decided to drag half of GNOME along as a dependency.

But “immutable base” creates a tension: the system is stable because you can’t easily change it, which means your development tools need to live somewhere else. Containers are the standard answer. And there are good tools for that — I’ll get to them in a moment.

The problem is: most container-based dev environment tools give you a container. They don’t give you a specific, identifiable, reproducible container. You get “Ubuntu with some stuff installed.” You don’t get “this exact resolved set of packages, with this base image digest, producing this deterministic identity that I can verify later.”

That’s the gap Karapace is trying to fill.

🔗What already exists (and why I built something anyway)

Before writing a single line of Rust, I looked at what’s out there. And there’s a lot.

🔗Toolbox and Distrobox

Toolbox is the Fedora/GNOME-adjacent answer to “I need dev tools on my immutable system.” It’s built on top of Podman and OCI containers, and it gives you seamless access to your home directory, Wayland, X11, networking, D-Bus — basically, the container feels like your host, but with a different userland.

Distrobox takes the same idea further. It describes itself as “a fancy wrapper around podman, docker, or lilipod,” and it works with a broader range of container runtimes. It explicitly references Toolbx as a conceptual ancestor, but it’s written in POSIX sh for maximum portability.

Both are good at what they do. But what they do is: give you a persistent, host-integrated container. They don’t track what’s in the container in a deterministic way. There’s no lock file. There’s no content-addressed identity. If you install gcc and cmake inside your Toolbx container today, and I do the same tomorrow, we’ll probably get the same result — but “probably” is doing a lot of work in that sentence.

🔗KDE Kapsule

Kapsule is a newer project by Lasath Fernando, and it’s solving a related but distinct problem. It’s an Incus-based container management tool with native KDE/Plasma integration, designed specifically for KDE Linux.

Lasath’s blog posts lay out the reasoning clearly: KDE Linux is immutable, Flatpak handles GUI apps, but CLI tools and dev environments need a different model. Kapsule’s answer is Incus containers with deep desktop integration — Konsole profiles that open directly into containers, D-Bus passthrough, plans for a System Settings module.

It’s a really well-scoped project, and its priorities are explicitly about desktop integration and user experience on KDE Linux. Karapace’s priorities are different: I care less about “the container feels like my desktop” and more about “the container has a deterministic, verifiable identity.”

🔗mise (mise-en-place)

mise is a polyglot tool version manager — it replaces tools like asdf, nvm, pyenv, and rbenv. It’s excellent at what it does: managing which version of Node, Python, Ruby, or Go is active in your shell.

But it’s not building containers. It’s not creating isolated environments. It’s managing tool versions on your host (or in your shell session). Karapace and mise operate at completely different layers of the stack.

🔗Homebrew

Homebrew on Linux is a package manager. It lets you install software that isn’t packaged by your distribution, or get newer versions when your distro’s repos are behind.

Again, different layer. Homebrew is about getting packages onto a system. Karapace is about defining an isolated environment and giving it a deterministic identity. You could, in theory, use Homebrew inside a Karapace environment — they’re not competitors, they’re different categories.

🔗So what does Karapace actually do?

The README describes it as:

Deterministic, content-addressed container environments for Linux. No root. No daemon.

Here’s what that means in practice.

You write a karapace.toml manifest. It declares your base image, the packages you want, mount points, hardware access, resource limits — the full description of what the environment should be.

When you run karapace build, the engine resolves that manifest: it downloads the base image, pins the exact content digest, queries the package manager for exact versions, and writes a karapace.lock file. This is the resolved state — no ambiguity, no “latest,” no implicit defaults.

Then it computes an env_id: a blake3 hash of the resolved lock file. Not the manifest (which is what you asked for), but the lock (which is what you got). The architecture docs describe exactly what goes into that hash: the base image digest, resolved package versions, apps, hardware flags, mount configuration, backend selection, network isolation settings, and resource limits — in a defined order.

This is the core idea: the environment’s name is derived from its contents. If you change anything, the env_id changes. If you don’t, it doesn’t. Two machines that resolve the same manifest to the same lock file will compute the same env_id.

The build and reproducibility docs are explicit about what this guarantees and what it doesn’t: the identity is reproducible (same inputs → same env_id), but the filesystem is not guaranteed to be bit-for-bit identical, because package managers can produce different file layouts across runs.

That’s an honest distinction, and I think it’s an important one. Claiming bit-for-bit reproducibility when you’re pulling packages from a distro repository would be misleading. What Karapace does guarantee is that the identity — the resolved description of what should be in the environment — is deterministic.

🔗The store: where environments live

Karapace keeps everything in a local content-addressed store. By default it lives at ~/.local/share/karapace, and it’s organized around blake3-keyed objects, layer manifests, environment metadata, and a write-ahead log (WAL) for crash recovery.

I like this design because it means the store is self-verifying. Objects are keyed by their content hash. If something gets corrupted, karapace verify-store can detect it. It’s the same principle behind Git’s object store or Nix’s store paths — content-addressing gives you integrity checking for free.

🔗Runtimes: how environments actually run

The architecture docs describe three runtime backends behind a common trait:

  • namespace (the default): uses unshare + fuse-overlayfs + chroot. No daemon, no root, no container runtime dependency. This is the “works on any Linux with user namespaces” option.
  • oci: delegates to rootless OCI runtimes like crun, runc, or youki. For when you want the OCI ecosystem but still want Karapace’s identity model.
  • mock: deterministic stubs for testing. Because testing container runtimes without actually running containers is a surprisingly useful thing.

The default backend is the one I care most about. It’s the “no root, no daemon” promise from the README: you don’t need Docker, you don’t need Podman, you don’t need any system service. Just Linux user namespaces and FUSE.

🔗Snapshots: because determinism doesn’t mean rigidity

There’s a tension in any “deterministic environment” tool: if the environment is supposed to be defined by its manifest, what happens when you actually use it and install things, change configs, experiment?

Karapace handles this with snapshots. You can karapace commit to capture the current state of the writable overlay as a snapshot layer, and karapace restore to roll back. You can check drift with karapace diff.

It’s a practical escape hatch. The environment is designed to be defined and repeatable, but real development involves experimentation. Snapshots let you try things without losing the ability to go back.

🔗Security: documented boundaries, not marketing claims

The security model is one of the parts I spent the most time on — not because Karapace is trying to be a security product, but because I think it’s irresponsible to ship isolation without documenting what it actually isolates.

The docs describe mount validation (allowlisted paths, default prefixes for /home and /tmp), device policy (GPU and audio toggles), environment variable filtering, store integrity verification, and signal handling.

But more importantly, they also document what is NOT protected: the security model page includes an explicit list of trust assumptions and boundaries. If the host kernel is compromised, Karapace can’t help you. If a process inside the container can escape user namespaces, that’s a kernel bug, not a Karapace bug. I’d rather be honest about that than pretend the tool provides guarantees it doesn’t.

🔗What’s built today

Karapace is at version 0.1.0. The README and getting started guide walk through the full workflow: karapace newkarapace pinkarapace buildkarapace exec / karapace enter. Inspection (list, inspect), drift checking (diff), snapshots (commit, restore), garbage collection (gc), store verification (verify-store), and a TUI are all implemented.

The CLI reference documents over 25 commands, global flags, environment variables, and exit codes.

It’s a real tool. It works. It’s not finished.

🔗What’s not built yet (being honest)

The README is upfront about current limitations:

  • Linux only. This one’s not going away anytime soon — the whole isolation model is built on Linux user namespaces and FUSE.
  • Layer packing drops extended attributes, device nodes, hardlinks, SELinux labels, and ACLs. The storage format docs explain what gets stripped and why.
  • Base images are content-hashed but not GPG-verified. The integrity is there (blake3 digest), but the authenticity isn’t (no signature verification yet).
  • No MAC enforcement (SELinux/AppArmor) inside containers.
  • Remote protocol has no authentication yet. The push/pull commands exist, but the protocol doesn’t verify who’s on the other end.

These are all real gaps. Some of them (GPG verification, remote auth) are the kind of thing that belongs on a roadmap. Others (Linux only) are architectural realities.

🔗Why Rust, and why from scratch

I could have wrapped an existing container runtime and added a lock file on top. I considered it.

The reason I didn’t is that the identity model — the part where env_id is derived from the resolved lock file — needs to be deeply integrated with the build pipeline. It’s not something you can bolt on after the fact. The architecture docs describe the build pipeline as a single engine that goes from manifest parsing through normalization, resolution, locking, identity computation, store operations, filesystem building, and lock file writing — all as one coordinated flow.

Rust was the natural choice for the same reasons it usually is: I wanted a single static binary, I wanted the type system to enforce invariants at compile time, and I wanted the performance characteristics of native code for things like blake3 hashing and overlay management.

The project is structured as a Rust workspace with 9 crates, each with a clear boundary. That’s not because I love over-engineering — it’s because the alternative (one giant crate where the CLI, the engine, the runtime backends, the store, and the schema all live together) would have been unmaintainable by the third week.

🔗The bigger picture

Karapace exists because I think “dev environment” should be a first-class, addressable, verifiable concept — not just “a container with stuff in it.”

Toolbox and Distrobox are great at giving you a container that feels like home. Kapsule is great at integrating containers into the KDE desktop. mise is great at managing tool versions. Homebrew is great at installing packages.

Karapace is trying to be great at a different thing: making the environment itself an artifact with an identity.

If that sounds useful to you, the code is at https://github.com/marcoallegretti/karapace, and the docs are thorough enough that you can judge for yourself whether the implementation matches the claims.