Six and a half months after the first announcement, 22 betas and 2 rcs later: incus-compose is 1.0.0.

Three Rewrites ended on a line I want to pick back up. “go-orb: three years of work. No users. incus-compose: two months. Several.” That stayed true. People ran it, broke it, filed issues — and the core from The Core Should Know Nothing just kept absorbing what they threw at it. New resources, new edge cases, no changes to the core itself.

1.0 means the commands people already know from Docker Compose exist here too: up, down, start, stop, restart, logs, exec. It pulls images from docker.io and ghcr.io, builds locally through Podman or Docker, does health checks and restart policies through a small sidecar called ic-healthd, and shifts UID/GID on volumes so bind mounts behave. None of it is exotic. Compose already solved these problems — I only had to bring them to Incus without losing what makes Incus Incus: system containers, VMs, real isolation.

Somewhere around beta19, @blurry needed a CIFS network share mounted into a container. Incus’s normal disk device couldn’t idmap it — the kernel just refused. I didn’t build CIFS support. x-incus let him drop raw LXC config straight into the instance, lxc.mount.entry = ... rbind,idmap=container, past incus-compose entirely for that one device. When idmap still fought him, the real fix was smaller: an opt-out from shifting, so the mount doesn’t need idmap at all. That’s the whole philosophy of x-incus — build the wrapper, but never weld the escape hatch shut.

The part I didn’t plan for: Windows and macOS. incus-compose and the incus client are plain Go binaries. No Docker Desktop, no WSL, no local Linux VM — point a Windows laptop at a remote Incus host over HTTPS and it runs.

What’s left shows up only after real usage wears the edges smooth. healthd still polls instead of listening to Incus’s own event API. There’s no backup command yet. run, pause/unpause, port are still open ground — small enough to be a first contribution.

@bketelsen’s proof-of-concept is where this started. @stgraber pulled the project into the lxc organization. @pyrodogg caught the bind-mount seeding problem — silently copying host files into a volume severs the connection to the original directory, the wrong default for a media library — and got it turned into opt-in. @blurry chased unmanaged bridges and CIFS shares until USB and GPU passthrough existed, for a Frigate NVR setup running hardware I don’t own. @Sagi, @neitsab, @kgoetz, @edorgeville, @bburky — 22 betas exist to be broken, and you broke mine in ways I wouldn’t have found alone.

Hours before release, Brian showed up in the thread again — the one whose proof-of-concept started all this, quiet for months. “It looks like you’ve solved all of the really big problems.” That’s the whole arc in one sentence, from the person whose own version never made it this far.

I run this in production myself: Immich, Wiki.js, LeafWiki, Gitea, Caddy, Kimai, a DNS stack. That’s the test suite that matters.

Boring, shipped.


By René Jochum and Claude (Anthropic). License: CC-BY-4.0.