All notable changes to incus-compose are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Version numbering moved from 0.0.1 to 1.0.0 at beta11 (1.0.0 is the intended
final version), and the beta suffix gained a dot (beta.16) from beta.16 onward
for correct semver ordering. Headings below preserve each release's announced
form.
- A service whose image ships a Dockerfile
HEALTHCHECKand whose compose file says nothing about health now inherits that check, asdocker composedoes. The service is watched by ic-healthd, so with a restart policy set a check nobody reviewed can restart it, and adepends_on: condition: service_healthyon it now resolves on the image's check rather than on its run state. Opt out withhealthcheck: {disable: true}, which is also honoured now, or withuser.healthcheck.enabled: "false"viax-incus. The image's ownHEALTHCHECK NONEis honoured the same way. Images already in the cache are not re-read, so this reaches an existing stack only on a fresh pull, and only when the instance is created. (by @jochumdev)
-
healthcheck: {disable: true}no longer enrolls the service for health checking and writes anulltest command. The compose spec's own way to say "no check" was read as a check being declared. (by @jochumdev) -
ic-healthd runs a healthcheck as the image's own process - its working directory, user and group (
oci.cwd,oci.uid,oci.gid) - instead of as root in/root, which is where anexeclands. A test using a relative path failed outright, and one depending on the service's user could pass as root where it would not have for the service itself. Both are normal ways to write a test, because that is how docker runs them. (by @jochumdev)
cp,topandeventsare actually there. v1.3.0 documented and announced all three, but the commit adding them never landed in the release. (by @jochumdev)
-
services.{name}.x-incus-compose.profiles: set the instance's full Incus profile list on create, verbatim - same semantics asincus launch --profile, so a list that omitsdefaultleaves the instance without it. Until now nothing in compose could express profile membership:x-incusmerges into the instance's config map, andprofilesis a field on the instance struct, not a config key. (by @alien43) -
run SERVICE [COMMAND]starts a one-off instance and exits with the command's own status. One-off instances carryuser.incus-compose.oneoff=true:upnever reconciles them,pslists them under their service,downremoves them without--rm, and ic-healthd never restarts one. The command runs through an exec into a blocking helper, so it is not PID 1;pullandupprefetch that helper, which is the only step needing the network, so an air-gapped site canrunlater.--init/INCUS_COMPOSE_INIT_IMAGE/x-incus-compose.initpoint it at a mirror. A cluster mixing CPU architectures is not supported. (by @jochumdev) -
pause,unpause,kill,cp,top,eventsandport, matching theirdocker composecounterparts. A pause also setsuser.healthcheck.stopped, since a paused instance answers no healthcheck and ic-healthd would restart out of the pause.kill -stakes only SIGKILL, in docker's three spellings: the Incus state API carries no signal.cpdecides which side is the instance by the name before the colon, so a Windows drive stays local.topreports per instance where docker reports per process. (by @jochumdev) -
Two commands docker compose has no counterpart for:
port-forward SERVICE TARGET_PORT [LISTEN_PORT]runs a local TCP listener and forwards into the instance, reaching a port that was never published - it needs Incus 7.3 or 7.0.1 LTS - andhealthd statusprints the shared daemon's health status key. (by @jochumdev) -
backupcopies a project's named volumes into a separate<project>-backupproject with per-run restore points, wheredown --volumesanddown --projectcannot reach them.create,list,verify,restoreanddelete --keep-last N; the pool comes fromx-incus-compose.backup.pool. (by @ishaan-jindal and @jochumdev) -
Volumes are filled from the image. Every path an image declares as a
VOLUMEgets a storage volume of the service's own, instead of the tmpfs that lost its contents on restart, and a named volume starts from what the image ships at its target -conf:/etc/nginx/conf.dis no longer empty on the first run.x-incus-compose.auto-volumes: falseandvolume: {nocopy: true}turn each off. (by @jochumdev) -
An external network can name
<project>:<network>to attach to a managed network owned by another compose project. (by @jochumdev) -
ic-healthd is published for
ppc64le,s390xandriscv64besidesamd64andarm64. (by @jochumdev)
-
stop, and with itrestart, shuts a service down gracefully and kills it once--timeoutis up. Both killed outright before, so--timeoutdid nothing at all.killis the old behaviour under its own name. (by @jochumdev) -
--pull alwaysonly re-fetches an image the registry moved, rather than dropping every registry image and downloading it again per run, andup --pull alwaysrecreates the services whose image it replaced. A registry the client cannot reach leaves the stored image alone instead of failing. (by @jochumdev and @alien43) -
ic-healthd treats Incus's
instance-resumedas a start, sounpauseputs a service back under watch at once instead of at the next resync. (by @jochumdev) -
A config or secret setting
uidwithoutgidleaves the group at 0, the way docker does, instead of taking the instance's. (by @jochumdev) -
Waiting for a container's IP reports a clearer timeout, naming the likely cause and a command to check it. (by @jochumdev)
-
The image cache is keyed by architecture, so a cluster mixing architectures no longer serves one member's image to all of them. An OCI pull is pinned to the manifest digest for that architecture; a source that cannot be pinned - simplestreams, a native
incus:remote - is checked once stored and deleted again. Arch-blind entries from an earlier version are re-fetched once and left in the cache until deleted by hand. (by @jochumdev) -
platform:is honoured for pulled images, not only built ones, in the OCI spelling docker uses (linux/arm/v7), and an unsupported one is an error.arm/v6andarm/v7resolve to the right manifest, and two services wanting one image reference for different architectures is reported rather than served from whichever was configured first. (by @jochumdev) -
user:may name its user and group (user: "netbox:root"), resolved against the image's own/etc/passwdand/etc/group, and so may the image's ownUSER. A name the image does not define is an error rather than a silent fall back to root; a name with no group takes that user's own group, a number with no group keeps GID 0. (by @jochumdev) -
A config or secret whose target sits inside a volume is written into that volume, instead of into the instance's filesystem where the mount hid it. (by @jochumdev)
-
upno longer hangs until the start timeout on a service already reported healthy. The wait for a container's IP refreshed the shared instance state as it polled, overwriting the verdict a dependent service was waiting for, and now reads into a state of its own. Health-check dependencies also wait for ic-healthd's own checks to pass, not only for its instance to beRunning. (by @jochumdev) -
A health check could fail with
websocket: bad handshakeinstead of reporting its result: the exec control socket was dialed in map order, so a fast command could finish and retire the operation first. (by @jochumdev) -
healthd upstops at the first resource that fails, and no longer leaves a storage volume behind that every later attempt fails on withUID mismatch. (by @jochumdev) -
An OCI image whose
CMDis empty is no longer re-read from its registry on every run. (by @jochumdev)
-
Incus 7.0.1 (LTS) or 7.2 is now the minimum, checked when a command connects rather than failing somewhere further in. Older daemons have no
oci_network_config, which every compose network attachment relies on for static addresses, gateways andoci.dns.*. (by @jochumdev) -
The bridge the shared ic-healthd daemon attaches to is now called
icompose0rather thanic-healthd. A daemon that is already running keeps its current bridge until it is recreated, and the old network is not removed for you. (by @jochumdev) -
up --no-startnow returns once the containers are created, as--detachdoes. It used to go on to follow logs from instances it never started, and the interrupt that ended that stream tore them down again. (by @jochumdev) -
up --buildnow recreates the instances of the services whose image it rebuilt, so the new image is what they run. Previously the image was rebuilt but the existing instances kept the old one until--recreatewas passed as well. A service that only consumes an image another service builds is recreated too; everything else is left alone. (by @jochumdev) -
--pull alwayson an image from an OCI registry re-downloads it rather than keeping a cached copy whose digest still matches. Deciding that needed a client-side registry lookup; resolving the reference is now left to the Incus server. Nativeincus:remotes are unaffected. (by @jochumdev)
-
sysctls:on a service is mapped tolinux.sysctl.*on the instance, applied immediately and kept across a restart. (by @alien43) -
x-incus-compose.gateway: falseon a service's network attachment allows a staticipv4_address/ipv6_addresson a network that declares no CIDR. That combination is otherwise rejected, because the gateway is not known until the network exists. (by @jochumdev)
-
A NIC device carrying
nictype(bridgedwith aparent, say) and no managednetwork:is accepted instead of rejected. Incus takes such a device on its own, and it is the only way to attach an instance to an unmanaged host bridge. (by @alien43) -
A failed image build says what failed. The lock the build takes first reported the daemon's error unwrapped, so anything wrong with it read as a bare
not foundagainst the image being built; it now names the lock volume and the project it lives in, and--debugreports which stage the build reached. (by @jochumdev) -
name:on a network now selects the Incus network it names, external or managed. It was documented but never read, so onlyx-incus-compose.networkhad any effect; an explicitname:now wins over that extension. (by @jochumdev) -
Working on several services at once no longer races. Every worker drove one shared Incus client, whose event-listener state cannot be used from more than one goroutine; each has its own connection now. The races that sat on top of it are gone with it: two workers setting up the image lock volume at the same time, simultaneous starts resolving which ic-healthd watches the project, and a wait for an instance's addresses that trusted a lifecycle event and stalled DNS registration until the timeout when one arrived late. (by @jochumdev)
-
ic-healthd gives up on an Incus call that stops answering instead of leaking the goroutine waiting on it, so a health check or a restart that times out no longer costs the daemon anything. A probe abandoned mid-command also has its exec canceled, rather than leaving it running in the container. (by @jochumdev)
-
ic-healthd waits for the instance's operation lock to clear before retrying a write it rejected, instead of retrying on a fixed delay that could expire six times while a slow stop was still running. (by @jochumdev)
-
Loading a compose file reads the server's API extensions once at connect rather than once per service, network and published port. (by @jochumdev)
-
Waiting for an image to appear in the cache gives up after the five minutes it was meant to, and stops when the command is canceled. The retry took its delay as a starting point for exponential backoff and ignored cancellation, so ten attempts could span hours that no interrupt would end. (by @jochumdev)
-
A start or stop held up by another operation on the same instance no longer spends its
--timeouton backing off. The wait for the instance lock is server-side and already correct; the retry around it doubled its delay on top, up to two minutes, which could turn contention it would have ridden out into a reported timeout. Waiting for ic-healthd to come up is likewise the three seconds it claims rather than fifteen. (by @jochumdev) -
A built image now carries its environment into the instance. The image's
ENVwas dropped on the way in, so a service built from a Dockerfile came up without thePATH,HOMEandTERMthe same image pulled from a registry gets. (by @jochumdev)
- The shared ic-healthd runs in an Incus project of its own,
incus-compose, on a bridge of its own,ic-healthd, with its own root disk. It took all three from thedefaultproject'sdefaultprofile before, so a server whose default profile uses an unmanaged bridge - or no NIC - could not bring the shared daemon up at all. No instance or volume of ours lands in thedefaultproject any more. (by @jochumdev) --healthd-network/x-incus-compose.healthd.networknow applies to the shared daemon too, and a network the compose file declares is created before the daemon attaches to it. It was warned about and ignored outside project scope. Likeincus,workersandx-incus, the first project to bring the shared daemon up supplies it. (by @jochumdev)- ic-healthd's default pool sizes are
workers: 128andrestart-workers: 32, up from32and12. One daemon now watches every project, so the caps are fleet-wide and the old ones queued behind a handful of slow projects. (by @jochumdev)
Upgrading from
v1.2.0-rc.1orrc.2- those left a daemon in thedefaultproject, and nothing moves it for you. Runincus-compose healthd down --forcebefore upgrading, or afterwards delete theic-healthdinstance and volume in thedefaultproject and itsic-healthd-globalcertificate. Two daemons watching the same projects otherwise both restart the same instances. Releases beforev1.2.0-rc.1had no shared daemon and need nothing.
upno longer fetches the ic-healthd image when the daemon already runs the one asked for. It pulled on every run, so a tag that had gone from the registry - or a registry that was simply unreachable - failed the whole project even though the daemon was healthy and nothing needed replacing. (by @jochumdev)- Containers on a network that pins its own subnet can reach the outside again.
Incus turns
ipv4.nat/ipv6.naton only for a subnet it picked itself, so a network given an explicitipv4.addressthroughx-incuscame up without NAT and nothing on it could route out. Both now default totruefor any non-internalnetwork, matching docker; set them inx-incusto say otherwise. Networks that already exist keep the setting they were created with. (by @jochumdev)
self-updateinstalls the build for the machine it runs on. It picked the first asset in the release instead -darwin_amd64for everybody - so on Linux and Windows it replaced the binary with one that cannot execute. The brokenself-updateis the one already installed, so 1.0.0 and 1.1.0 users have to reinstall once withinstall.sh; it works from here on. (by @jochumdev)
-
ic-healthd watches several projects from one event listener, and
--projectis now optional: without it it watches every project whose config matches--project-marker, by defaultuser.healthcheck.scope=global. The flag takes aKEY=VALUEpair now; a bare key still meansKEY=true. See Health Checking. (by @jochumdev) -
x-incus-compose.healthdgainedscope,workers,restart-workersandx-incus.workers/restart-workerssize the daemon's pools;x-incusis Incus instance config for the sidecar, e.g.limits.cpu. (by @jochumdev) -
upandhealthd upupgrade the ic-healthd container: when the image you ask for is a newer release than the one it is running, the daemon is replaced by one built from it. The comparison is semver and forward-only, so a machine on an older incus-compose cannot downgrade a daemon shared with everyone else. Tags that are not release versions - moving tags likelatest, andgit describebuilds - are not comparable and replace on any difference.The replacement keeps the running daemon's configuration - its endpoint, worker counts, limits and anything else set on it - so an upgrade triggered by one project no longer resets settings another supplied. A flag or compose value given to the run doing the upgrade still wins, and a limit below the sidecar's own default is raised to it. (by @jochumdev)
-
the
healthdsub-commands run without a compose file, acting on the shared daemon.incus-compose healthd upon a bare server creates it before any project exists;logs,restart,reloadanddownfail withno ic-healthd is runningwhen there is none instead of complaining about a missingcompose.yaml. (by @jochumdev) -
--trace, a level below--debug(which it implies), on both incus-compose andic-healthd run. The daemon's per-event and per-check lines moved there, so--debugstays readable on a server watching many projects;--traceis what shows the Incus events arriving when a project is not being watched. incus-compose passes it to the sidecar asINCUS_COMPOSE_HEALTHD_TRACE; it has no level of its own in incus-compose yet. (by @jochumdev) -
healthd down --forcestops the shared daemon without the confirmation prompt. Without it, taking down a daemon other projects rely on asks first, and refuses outright when there is no terminal to ask on. (by @jochumdev) -
entrypoint:is supported and follows the compose spec.command:on its own still appends rather than replacing. See Compose Compatibility. (by @jochumdev) -
--pull neveronup,buildandpullnever contacts a registry, for air-gapped use.pull --policyis honoured instead of ignored. (by @jochumdev)
-
one ic-healthd for the whole server.
upno longer creates a sidecar per project. It creates a single shared daemon namedic-healthdin the Incusdefaultproject and marks the projectuser.healthcheck.scope=globalso the daemon picks it up. A project that already had its own sidecar has it removed, before the mark is written, so the two never watch it at once.Keep a sidecar of your own with
up --healthd-scope projectorx-incus-compose.healthd.scope: project; that is also the way to keep the daemon's Incus certificate restricted to one project, since the shared one is unrestricted by necessity. Whichever a project uses is stored on the project and wins over the flag and the compose file from then on, so changing it later means changing that key.Projects last brought up by an earlier version carry no scope at all and are invisible to the shared daemon, so nothing changes for them until you run
up. See Health Checking. (by @jochumdev) -
health checking is opt-in. ic-healthd watches an instance only when it carries
user.healthcheck.enabled: "true"; ahealthcheck:block or a restart policy alone is no longer enough. incus-compose writes it automatically.Instances created before this do not carry the key. If a project uses
healthcheck:or a restart policy, runuponce to have those enforced again; it adds the key in place, with no--recreateand no downtime. Projects that use neither need nothing, and containers keep running either way. See Health Checking. (by @jochumdev) -
ic-healthd caps the checks and restarts it runs at once, over every project it watches, with
--workers(32) and--restart-workers(12). An action with no worker free is retried rather than queued, so it never counts as a check that timed out.The sidecar is created with 2 CPUs and 256MiB instead of 1 and 50MB, and an existing one is raised to that when it is replaced. Only a project-scoped sidecar counts against an aggregate
limits.cpu/limits.memory; the shared daemon lives in the Incusdefaultproject and counts against nothing. (by @jochumdev) -
user.healthcheck.statusis written by ic-healthd alone. incus-compose no longer stampsstarting/stoppedon it, so the value always says what a daemon actually saw: an instance carries no status until one reports, reportsstoppedwhile it is down, andunknownfor good underup --no-healthd.listshows those asUnknownandStopped. (by @jochumdev) -
the image cache moved from the Incus
defaultproject toincus-compose-cache. Whatever earlier versions cached indefaultstays there unread; delete it by hand. (by @jochumdev) -
a service with
build:no longer rebuilds in every project - the cache is checked before the builder runs. Use--buildto force a rebuild. (by @jochumdev) -
upwithout--detachnow matchesdocker compose: create, start, stream logs, anddownon interrupt. (by @jochumdev) -
config --format=jsonkeeps thex-incusandx-incus-composeblocks, whichdocker composedrops. Parse the JSON rather than diffing it against docker's. (by @jochumdev)
- ic-healthd reliability: stalled API calls, checker cancellation races, invalid
intervals,
unless-stoppedmisread as a deliberate stop, instances re-checked forever after they stopped, and state lost on an event-listener reconnect. (by @jochumdev) - concurrent
upruns no longer fail creating the same volume, profile or network. (by @jochumdev) build.dockerfileis resolved relative tobuild.context, not the working directory. (by @jochumdev)command:arguments containing spaces, quotes or$are shell-quoted correctly instead of being re-split. (by @jochumdev)- a
configs:orsecrets:entry whose target already exists in the image is written instead of silently skipped. (by @jochumdev) - a static
ipv4_address/ipv6_addresson a network with no explicit address now fails with an explanation instead of producing a broken NIC. (by @jochumdev) config --format=yamlno longer nests the document under aproject:key. (by @jochumdev)- default storage pool detection checks the
defaultprofile's root device first. (by @jochumdev) - a service's
x-incus.raw.dnsmasqlines are no longer appended twice. (by @jochumdev) - pushing directory content into a storage volume no longer closes each file twice. (by @jochumdev)
build:now works on every platform instead of Linux/Unix-only: image building previously unpacked the built OCI image withumoci(gated tounix && !darwin, with a "not implemented" stub elsewhere), and now instead derives the minimalconfig.jsonneeded by Incus's LXC driver (Process.Args/Cwd/User) straight from<builder> inspect, dropping theopencontainers/image-specandopencontainers/umocidependencies. Builder detection also no longer shells out to<builder> versionto distinguish Podman from Docker; it now checks the resolved binary name, andbuildahis tried alongsidepodman/docker. (by @jochumdev)- Published ports (
ports:) create a proxy device. By default this is a userspace proxy targeting the container loopback (nat=false, connect127.0.0.1); per-portx-incus-compose.nat: trueopts into NAT mode instead, connecting via ARP/NDP-based instance IP detection (needs Incus 7.2 or 7.0.1 LTS) or the NIC's static IP directly if one is configured (same version floor). Requestingnaton a server below that floor skips the port with a warning instead of silently falling back.natwas previously auto-enabled on Incus 7.0+; it's opt-in now because NAT mode doesn't work for host-sidelocalhost:<port>access — it routes to the instance's real address, not the loopback interface. (by @ishaan-jindal) ic-healthdis now event-driven instead of poll/SIGHUP-based: it discovers instances once, then reacts to the Incus lifecycle event stream (start, stop, shutdown, delete) to keep its tracked set in sync, spawning or killing checkers for exactly the delta. A checker only probes and reports its own status; the runner alone decides whether to restart an instance.incus-compose healthd restartno longer needs to register a client-side reloader hook, since healthd resyncs itself from events. (by @jochumdev)up's wait for a service's own healthcheck to become healthy, and its wait ondepends_on: { condition: service_healthy }dependencies, no longer poll Incus every 500ms. The client now opens a project-scoped Incus lifecycle event listener and blocks on a per-instance condition variable that's broadcast whenever that instance's state is refreshed from a lifecycle event (start/stop/update), cutting idleGetInstanceAPI calls duringup. (by @jochumdev)- Image caching: images built with
build:now go through the same cache path as pulled images instead of being created directly in the project, fixing stale/duplicate builds when a cache is configured. Usebuild.no_cache: trueto disable caching.deleteCachedalso no longer aborts before cleaning up the cache when the source image was already removed. (by @jochumdev)
x-incus-compose.nat-proxyextension and all associated post-start device attachment machinery. Ports are now handled entirely through the standardports:field. (by @ishaan-jindal)
healthd upnow recreates the ic-healthd sidecar when the running instance's image no longer matches the configured one, instead of leaving it on the stale image until a manualhealthd downfirst.upshares the same code path, so a plainupalso picks up healthd image updates automatically. (by @jochumdev)services.{name}.configs/ top-levelconfigs:: mount config files into the container, sourced from a file, inlinecontent, or an environment variable.modedefaults to0444(world-readable); the writable bit is always ignored per the compose-spec, even if an explicitmodeis set. (by @ishaan-jindal)- Well-known OCI registries (
docker.io,ghcr.io,mcr.microsoft.com,quay.io,registry.gitlab.com) are now auto-added to the in-memory Incus CLI config when an image from that registry is used, removing the need for manualincus remote addsteps. (by @ishaan-jindal) - Do not ignore healthd in
up --no-deps <service>it allows script to wait on the service to be ready. Useup --no-deps --no-healthd <service>if you want the old behaviour. (by @jochumdev) x-incus-compose.healthd.external: true: the compose-file equivalent ofup --external-healthd/down --external-healthd, so a project can pin "bring your own healthd" permanently instead of passing the flag on every invocation. Combines with the flag by OR: either is enough to turn it on. (by @jochumdev)networks.{name}.aliaseson a service's network attachment now registers extra DNS names for its instance: each alias becomes acname=<alias>,<instance>record in the network'sraw.dnsmasq, resolving immediately instead of waiting on a DHCP lease like the existing service-name records. Works across networks shared between projects (external: true) without clobbering the other project's records. Limited to single-instance services, since a CNAME alias can only point at one target. (by @jochumdev)dns/dns_search/domainnamenow map to Incus'soci.dns.nameservers/oci.dns.search/oci.dns.domaininstance config keys, seeding the container's initial/etc/resolv.conf.dns_opthas no Incus equivalent and is not mapped. (by @jochumdev)
install.sh: fixed the checksum filename to match goreleaser's current release-artifact naming (checksums.txt), it was still using the old${PROJECT_NAME}_${VERSION}_checksums.txtpattern. (by @jochumdev)up --pull=alwaysandpull: the stale image was not always deleted from cache and project before re-copying, so a floating tag could keep serving the old image. Deleting the cache is now a distinct step that runs before create/refresh, and the well-known-registry hook fires on it too. (by @jochumdev)
.golangci.yml: enabled a much stricter linter set, and fixed the resulting findings across the codebase. (by @jochumdev)
The first stable release! hooray
- Refactored the whole image caching process, it's now doing the same as the incus client would do and allows disabling caching by setting it to empty.
self-updategot a--draftsflag and skips them by default.
Second release candidate cause of the breaking user. -> user.label. change
below.
- Labels now have a
user.label.prefix insteaduser.only, to not conflict with other user settings.
First release candidate. File pushes move to the Incus SFTP API, command now
layers on top of the image entrypoint instead of replacing it, and privileged
services are supported.
This is the first release that should actually work on Windows and MacOS.
E2E suite green.
services.{name}.privileged: true: run the container privileged (security.privileged).
- File pushes (secrets and single-file bind seeds) use the Incus SFTP API instead of the old REST file endpoint.
command:is appended to the image'soci.entrypointas arguments instead of overwriting it, matching Docker's ENTRYPOINT/CMD semantics.ic-healthdlogs more detail during operations.down --volumesnow deletes volumes while keeping the project; it is no longer an alias for--project. Use--projectto remove the whole project (and its volumes).listincludes the ic-healthd sidecar by default; the--healthdflag is replaced by--no-healthdto omit it.
healthd up/healthd downwork with custom networks.- Windows and macOS builds error cleanly instead of crashing on the umoci import.
healthd up --recreate; recreate the sidecar withhealthd downfollowed byhealthd up.
- CI runs slow tests with a 20m timeout and without parallelism to avoid overload; tooling paths and changelog links updated; lint fixes.
Pre-1.0 beta history (beta1 through beta.22, 2026-06-01 to 2026-07-06)
A real pull command, Docker-parity user handling and exec, plus
per-service raw devices and gateway selection.
E2E suite green still at ~60% coverage.
pullcommand: pre-pull service images (and the healthd sidecar) without creating anything, with--policy,--ignore-buildable,--ignore-build-failures,--no-healthd, and--with-deps.services.{name}.user: run the container process as a numericUIDorUID:GID(mapped tooci.uid/oci.gid).services.{name}.x-incus-compose.devices: attach raw Incus devices (gpu, unix-char, ...) verbatim; the requiredtypekey selects the device type.services.{name}.networks.<net>.x-incus-compose.gateway: true: places that NIC last so Incus uses its gateway as the instance's default route.
execruns as the instance's user/group by default (matchingdocker compose exec); override with--user/--group. The command and its arguments are passed to Incus verbatim, so leading-dash flags work unescaped.- Service network attachments are ordered deterministically (they previously followed Go map iteration order).
- Documentation moved to https://docs.incus-compose.org.
Standalone and bugfixed healthd, more x-incus reach, a native exec, and an error-severity system so recoverable problems warn instead of aborting.
E2E suite green, ~60% coverage.
x-incusextensions now pass through on service networks, service volumes, and devices, plus directtmpfson services (same verbatim key/value passthrough as instances and networks).- Standalone
ic-healthd: it now has its own tests and can run on its own. Env vars renamed to theINCUS_COMPOSE_HEALTHD_*prefix, and a--tokenflag was added. - Error-severity system:
Clone()andIgnoreError()let commands demote non-fatal problems to warnings instead of hard failures.up/down/start/stop/restartno longer abort on errors that don't matter. StackFailFast()andStack.SetOptions().- Exported
SanitizeProjectName().
execuses the nativeincus execimplementation instead of the in-house MVP terminal (~250 lines removed): better TTY handling and parity with theincusCLI.- Overridden network names are honored for normal networks too, not just special cases.
- OCI config is extracted after a build; resource dedup now keys on both
Name()andIncusName().
- Instance volumes land on the correct storage pool.
security.shiftedis left alone when the user has set it.progress.bypass()for all stdout/stderr fixes garbled output (#37).- DNS watcher is skipped when the service name equals the Incus name; no watcher for empty service names.
Internal project/stack refactor plus network-readiness and healthd reliability fixes.
E2E suite green, ~50% coverage.
- Instances wait for the network before starting via
raw.lxc=lxc.start.delay=1, fixing flaky startups where services came up before DNS/networking was usable.
- Reworked the ordering logic for
up/down/start/stop, with and without dependencies. Deliberate asymmetry:up/downfollowdepends_onby default (--no-depslimits to the named service);start/stop/restartact on the named service only (--with-depsmakes them followdepends_on). - Project no longer returns a
Stack; the CLI now owns stack assembly, with a new helper that adds resources in priority order. - Exported
SanitizeNetworkName.
- DNS update retries once on an ETag mismatch (concurrent-update race).
user.healthchecking.stoppedupdates go through a cleaner path; the hacky PATCH workaround is gone.
- deb/rpm/apk packages. Releases now ship the tarball/binary and install script only.
Mostly CLI and healthd fixes, plus event-driven log following.
- Event-driven
logs --follow: uses the Incus events API to attach and detach log streams as instances start and stop, no longer exiting when instances go away and picking up new instances automatically (#3).
down --projectnow deletes all resources (instances, networks, volumes, and the healthd sidecar) instead of relying on incus to do so.--debugno longer shows progress bars (they interfered with debug output).- DNS watcher waits up to 5s after a dnsmasq restart before starting the next instance.
- healthd: restart counting during the start period, instance tracking after
cancellation, and the
healthd up/healthd downlifecycle (#5).
- Automatic retry on client operations.
- The unnecessary
--with-depsflag fromlogs.
- Added a Terms section (#4); updated example healthchecks with
start_*directives; immich example now waits for DNS readiness and drops tini.
- Breaking: renamed the
--project-directoryshorthand from-pdto-P. - Breaking:
core.https_addressis now required (the server must be reachable over the network for image caching); the CLI warns when connecting over a unix socket. - Lowered the default
--workersfrom 10 back to 4 to avoid storage IO contention on cold-cache / large-image starts. - Use the non-
vversion for the healthd image while keeping thevprefix for incus-compose itself.
- Retry various client/CLI operations and tune the default timeouts; increased the delay between start/stop retries.
up --recreateno longer recreates networks.ic-healthdno longer shows up as an orphan inpsoutput.- healthd always restarts checkers on reload so new settings take effect.
- Silenced two noisy debug logs.
Hotfix on top of beta.16, focused on the health-check sidecar.
- Opt-out of volume shifting (
security.shifted) for cases where matching IDs inside the container don't matter.
ic-healthdnow runs inside the project stack and attaches to the project's own network (the project default unless overridden viax-incus-compose.healthd.network), ensured just before regular instances. Network extra options are no longer lost.
Requires updating via the install script (version format changed to beta.XX
for a correct semver version).
listnow has a separateHEALTHcolumn instead of appending health toSTATUS(columns: KIND, NAME, INCUSNAME, IMAGE, STATUS, HEALTH, ADDRESSES).- Every instance reports a health value; services without a healthcheck show "Unknown" rather than a blank.
up/startwait for the healthcheck to report healthy after starting an instance that defines one (polled every 500ms, bounded by--timeout), makingdepends_on: service_healthyreliable.ic-healthdreports its own status (healthy on start, unhealthy on shutdown) and locates the daemon instance via auser.healthcheck.daemonmarker.- New
--healthd-incusflag /INCUS_COMPOSE_HEALTHD_INCUSenv var to set the API URL the sidecar connects to (empty = auto-detect the IP from the attached bridge). - New top-level
x-incus-compose.healthdextension (incusAPI URL andnetworkas<project>:<network>or a plain bridge name; both default to the project's own network and the connection's port).
- Breaking: compose now defaults to incus listening on all interfaces; set
INCUS_COMPOSE_HEALTHD_INCUSto override. upreconciles the service count in both directions, matchingdocker compose up. Instances above the desired count (deploy.replicasor--scale) are torn down, highest index first. A manual--scale Napplies only to that invocation (#12).- Default
--timeoutraised from 10s to 1 minute.
- Breaking: the defunct
x-incus-compose.network-profileextension (replaced byx-incus-compose.healthd).
self-updatecommand: checks GitHub releases and updates the binary in place (release builds only, when the binary directory is writable).- Environment variables for all CLI flags; every global flag can now be set via
INCUS_COMPOSE_*(e.g.INCUS_COMPOSE_FILE,INCUS_COMPOSE_PROJECT_NAME,INCUS_COMPOSE_DEBUG). - Configurable worker count via
--workers/INCUS_COMPOSE_WORKERS(default 10).
.incus.yamloverlay loading:docker-compose.incus.yamlandcompose.incus.yamloverlays were not loaded correctly (#6).- Progress display and error rendering improvements (#7).
- healthd retry calculation:
retries = start_period / start_interval. - Small client-package cleanups.
- Improved healthd documentation.
- Network project/profile support:
x-incus-compose.network.projectandx-incus-compose.network.profilecontrol which Incus project and profile healthd uses.
- Breaking: bind-mounts are no longer seeded by default; they now default to
non-seeded (simple disk device pass-through). Set
x-incus-compose.seed: trueon the volume to restore copying files into the instance.
- Client connection stability: fixed several data races (reused
ProtocolIncus,noColorcontext var, random string generation). - Network profile fallback: use
devices.eth0.parentwhendevices.eth0.networkisn't available in a profile. - More robust healthd discovery (
FindHealthd). - Switched to
errors.As()for proper error unwrapping.
- Removed all remaining testify/suite usage; refactored
serviceToInstance()into smaller helpers; splitproject/project.gointo smaller files; removed dangling test fixtures; updated snapshots for the new bind-mount behavior.
- incus-compose is now part of the lxc organization on GitHub.
- The
mainbranch entered a feature freeze ahead of 1.0.0.
- Do not assume the availability of
incusbr0.
--image-cacheglobal flag (INCUS_COMPOSE_IMAGE_CACHE) to point the image cache at a different Incus project (default:default).--rmi/--imagesondownto remove project images on teardown, matching docker compose behaviour.- Extra storage volume config support via
x-incus-compose.
--with-depsscoping:up/downfollowdepends_onautomatically; all other commands require an explicit--with-deps.- Healthd is now skipped when no services require it.
- Build image name corrected to
localhost/<service>.
Moved to 1.0.0 from 0.0.1 (1.0.0 will be the final version). Mostly a health/dependency/lifecycle hardening release.
- Healthy-dependency support:
upwiresservice_healthydependencies into start ordering/waiting; new--dependency-timeoutflag. - Healthd expanded and rewritten: now runs when services use restart policies or
are depended on via
service_healthy, not only for an explicit healthcheck. Supportsstart_period,start_interval,interval,timeout, andretries; health state standardized viauser.healthcheck.status. shm_sizemaps to a/dev/shmtmpfs device.container_namesupport: used as the Incus instance name; scaled services becomecontainer_name-1,container_name-2, etc.- Added
examples/many-dependenciesandexamples/wikijs; movedtest/fixtures/immichtoexamples/immich; timestamped test logs undertest/logs/; addedjust test-slow.
- Breaking: removed the direct Incus URL env vars from the
documented/runtime connection flow (
INCUS_COMPOSE_URL,INCUS_COMPOSE_CERT,INCUS_COMPOSE_KEY). Use Incus CLI remotes instead (--remote,INCUS_REMOTE, or the default remote). - Breaking: timeout flags changed from integer seconds to Go duration values
internally (
up,down,start,stop,restart, healthd paths); use explicit durations like--timeout 10s. - Breaking: healthd sidecar name changed to
{project}-ic-healthd; scripts expecting plainic-healthdneed updating. - Improved CLI progress: healthd commands participate, progress moved to stdout and is less likely to corrupt logs.
- DNS watcher rewritten for concurrent per-action updates; instance IP handling refactored to support multiple interfaces/IP sets.
execnow targets the requested service instead of possibly choosing the wrong instance from the stack.list/psoutput is sorted for deterministic output (#46).- Progress no longer overwrites logs (#43), plus several writer/stdout/stderr fixes.
- Healthd stability: graceful deletion/reload, checker optimization, and the normal checker now starts correctly after the start-period checker succeeds.
- Instance lifecycle: fixes around already-running instances and recreate; Incus
timeout
0means "do not wait", so the internal default now maps to-1where needed. - Storage volumes: better shifted-volume validation, delayed from
Ensure()toStart()where runtime UID/GID data is available. - Image properties are copied correctly instead of sharing/mutating state.
- Several fixes around deleting project, external, and managed/dangling networks.
- Uses
errors.Is(err, ErrNotFound)in more places; better debug logging and wrapping.
Making Compose workflows more complete: local image builds, better progress feedback, and improved healthcheck restart behavior.
- Compose
build:support. New commands and flags:incus-compose build,incus-compose build <svc...>,up --build,up --no-build. Seedocs/build.md. - Progress output for all operations in the CLI.
- Breaking: no more Windows client support (a consequence of adding build; file an issue if you need it).
- Breaking: storage volume names changed to
vol-{name}(was{project}-{name}), hashed if longer than 59 chars. Existing volumes are not migrated automatically. ic-healthdrestart backoff (5s -> 10s -> 20s -> 40s -> 60s max) avoids tight restart loops for unhealthy services.restart: unless-stoppedhandled more correctly:stopmarksuser.stopped=true,startclears it, andic-healthdskips automatic restarts while a service was intentionally stopped.
- Bind-mounted directories now resolve through the actual Incus storage volume name.
- Copied files/directories use more normal permissions:
0644/0755. - Fixed a UID/GID copy bug.
Major image-handling rework, plus volume/bind-mount and healthd changes.
- OCI config is extracted at download time: UID, GID, entrypoint, and cwd are
read from a temporary stopped container right after download and stored as
image properties (
oci.uid,oci.gid, etc.) viaUpdateImage; later runs read them back from properties with no extra container. - Two-stage image cache restored (source -> cache project -> instance project),
along with
docs/architecture/client/image.md. - Deferred source/cache detection:
GetImageServerandEnsureProjectmoved fromnewImage()intoEnsure(), so no network connections happen during the configuration phase (fixing CI slowness). - Resource deduplication:
ResourceStore.Getnow compares byIncusName(), so normalized references (docker.io/nginx:alpinevsdocker.io/library/nginx:alpine) return the same object, preventing duplicate alias races. - Bind-mount files are pushed post-start via
InstanceFile; bind-mount directories become storage volumes withHostPathseeding on create.PostDevicesandActionPostEnsureare removed; volumes live inDevicesand are ensured beforeCreateInstanceFromImage. - Healthd resource removed: the
client.Healthdwrapper is replaced byhealthdUp/healthdDownhelpers incmd/incus-compose/healthd.go.KindHealthd,HealthdConfig, andresource_healthd.goare deleted. - Healthd instance is prefixed with the project name to prevent cross-project collisions.
- Skip token registration when a cert already exists (prevents repeated
re-registration on
ic-healthdrestart). downnetwork-listing failure during--projectdelete is demoted to a warning; a nil-check prevents a panic when listing fails.
- justfile version tags gained
--long --dirtyfor healthd builds.
- Removed the project name from storage volume names (manual migration required;
note beta10 renamed volumes again to
vol-{name}).
list --healthd: opt-in flag to include theic-healthdsidecar in list output.up --detach|-d: detach after starting services; logs are printed if not detached (#38).incus-compose incus proxy: pass-through command (incus --project={name} <xyz>) (#37).- Image name in
ps/list: the IMAGE column is populated for all instances, including the healthd sidecar, without requiring Image resources in read-only stacks (stored asuser.image_aliasat creation, resolved fromvolatile.base_imageas a fallback). - healthd resource limits: the sidecar is capped at 1 CPU and 50 MB RAM by default (required for project-wide limits).
ic-healthdnow compiles in and prints its version.oci-registry-cachepromoted to a standalone helper project.
healthd restartworks as intended: kill a service and healthd brings it back.- Breaking:
--no-pullreplaced by--pull(string flag) for docker compose compatibility.
downno longer deletes externally-managed networks.- healthd: fixed API endpoint resolution when connecting to the Incus socket (#39).
- Various fixes for projects that attach to pre-existing external networks.
incus-compose healthdsubcommand group for direct sidecar management:logs(stream),reload(reload health-check config),restart,up(recreate,--recreatesupported), anddown.- External network name override via
x-incus-compose.network: networks can declare their real Incus name independently of the compose key. Name resolution uses a 4-candidate probe (raw and sanitized, for both the override and the compose name) and locks in the first match.
- serviceName truncation regression: hyphenated service names (e.g.
my-service) were incorrectly stripped. Only trailing-{n}integer suffixes (the scaled instance convention) are now removed. ic-healthdnow appears inlist/psoutput.- Hardcoded default storage pool:
ic-healthdresources now use the client's configuredDefaultStoragePoolinstead of always"default". up --recreateon a healthd container no longer loses--incus/--projectOCI entrypoint flags;ResourceStore.Remove()is now called on everyDelete().
- Automatic loading of a
compose.incus.yamloverride file when present next to the main Compose file, keeping upstream Docker Compose files unchanged while adding Incus-specific configuration in a separate file.
- The
ic-healthdimage now usesbusybox:glibcinstead ofscratch.
x-incusoptions: Compose services can pass raw Incus instance config directly through to Incus (memory/CPU limits, nesting, security flags, etc.).- Automatic loading of the default incus profile.
- Project-wide
x-incus-compose.network-profilesupport (disables a per-project default network/bridge). - healthd reload on service changes.
- Network creation race that could cause dnsmasq failures in CI (avoids immediately updating a newly-created network before the old dnsmasq released its socket).
downnow deletes compose-managed networks when the project is brought down, fixing dangling networks.up --no-pullis now respected correctly (also ~2x faster test runs, 3min from 6min).
- Test coverage +17% (35% -> 52%).
- Kernel-mode NAT proxy for port proxies via
x-incus.nat-proxy(#30). - DHCP ranges and static IPv4 / IPv6 addresses on network creation.
- Healthd status in
ps/list. - Scaling now prunes dangling instances when
up --scalelowers the count (#34). logsomits old logs then follows when--followis set.- Small fixes to keep CI green.
Initial public beta. A ground-up Docker Compose workflow for Incus, inspired by Brian Ketelsen's proof-of-concept.
- Familiar commands:
up,down,start,stop,restart,list(andps),logs,exec,config, plusbuild,healthd,incus(pass-through), andself-update. - Compose project parsing via compose-go, with automatic
compose.incus.yamloverrides andx-incus/x-incus-composeextensions for raw Incus options. - Native OCI image pulling from docker.io, ghcr.io, and other registries.
- Two-stage image cache in a dedicated Incus project (survives
down/up, avoids registry rate limits). - Local image building via Podman/Docker.
- Bridge networks with automatic name sanitization.
- Static IPv4/IPv6 addresses with automatic DHCP ranges.
- Port forwarding via proxy devices or kernel NAT mode.
- Storage volumes with UID/GID shifting; bind mounts (pass-through by default, optional seeding).
- Health checks, restart policies, and
depends_on: service_healthyordering via theic-healthdsidecar. - Service scaling with
up --scaleand orphan pruning. - Incus project isolation.
- Resource limits and other advanced compose features (
shm_size,container_name, etc.). - Configuration via
INCUS_COMPOSE_*environment variables for every flag, with a configurable parallel worker count.