CHANGELOG.md
Rama develop · réplica de lectura del repositorio original.
# Changelog
All notable changes to incus-compose are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
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.
## [Unreleased]
### Added
- Support for the Compose Deploy Specification (`deploy.mode`,
`deploy.replicas`, `deploy.placement.constraints`,
`deploy.placement.preferences`, `deploy.placement.max_replicas_per_node`,
`deploy.restart_policy`, and `deploy.update_config` rolling updates) across
Incus cluster members. Scaling or multi-node placement on a cluster requires a
remote/shared storage pool unless overridden with `--allow-local-storage`
(`INCUS_COMPOSE_ALLOW_LOCAL_STORAGE`) or
`x-incus-compose.allow-local-storage: true`. (by @jochumdev, #1)
- Instances carry a `user.label.incus-compose.config-hash` label with the hash
of their service. `up` recreates the drifted ones, `config --hash` prints the
hash of the current file, and `ps` marks changed instances (`config_changed`
in JSON). (by @tallica)
- Support for declarative Network ACLs (firewall rules between compose networks)
via `networks.<name>.x-incus-compose.acl` (`default`, `ingress`, and `egress`
rules). Supports OVN-to-OVN network peer resolution (`@<network>/<peer>`) as
well as bridge subnet CIDR resolution across same-project and cross-project
(`<project>:<network>`) network references. (by @jochumdev, #98)
- Disk and tmpfs mount targets that are symlinks in the container rootfs (common
in `nix-containers` images such as `/etc/caddy` pointing into `/nix/store`)
are now materialized as real directories or files before starting the
instance, and recursive directory creation when pushing files now follows
existing symlinked parent directories. (by @jochumdev)
- Support for custom image builders and built-in `nix2container` builds via
`build.x-incus-compose` (`builder` and `args`), allowing container images to
be built directly from `nix2container` flake targets or external builder
binaries without Docker or Podman. (by @jochumdev)
- Support for OVN networks as the default network driver for newly created
projects when Incus supports OVN. Includes `--network-driver` /
`INCUS_COMPOSE_NETWORK_DRIVER` and `--network-uplink` /
`INCUS_COMPOSE_NETWORK_UPLINK` flags on `up`, top-level
`x-incus-compose.network.driver` (`auto`, `ovn`, `bridge`) and
`x-incus-compose.network.uplink` compose options (with backward-compatible
support for `x-incus-compose.network-driver`), and per-network
`x-incus-compose.uplink` (or `parent`) extension to configure uplink networks.
Existing projects and servers without OVN continue to use bridge networks. (by
@jochumdev)
- Service-name and alias DNS resolution now works on OVN networks. incus-compose
peers each project network to the shared `ic-dns` and scopes it with a hidden
network ACL, so service names and network aliases resolve with no compose
change. Network aliases are assigned to the first replica when scaled to
prevent contested collisions in `ic-dns`. OVN networks also get a default ACL
posture matching docker compose: instances on the same network reach each
other, nothing else may initiate in, outbound is allowed. (by @jochumdev)
- `--no-dns` / `INCUS_COMPOSE_NO_DNS` flag on `up` and `down`, and
`x-incus-compose.dns.disabled` compose option to opt out of DNS sidecar
creation/configuration on `up` and stop/removal of project-scoped sidecars on
`down`. (by @jochumdev)
- `ic-dns`: A new split-horizon authoritative DNS daemon for Incus instances,
built on the new `ievent` event framework and CoreDNS. Resolves instance names
dynamically within per-project or shared zones (`.incus`), serving records
based on querier network visibility and client subnet (RFC 7871 ECS). Supports
UDP and TCP on port 53, zone transfers (`--allow-transfer`), upstream
forwarding (`--forward`), configurable TTL, and Prometheus metrics and health
endpoints on `:9153` (or `--http`). Projects opt in via `--project-marker`
(defaulting to `user.label.dns.scope=global`), an explicit `--project` list,
or serve all visible projects with `--project-marker ""`. (by @jochumdev)
- `ievent`: A pluggable event pipeline framework for Incus lifecycle events,
processing daemon event streams through ordered, composable plugins (`source`,
`debounce`, `enricher`, `checker`, `dns`, `http`, `log`). (by @jochumdev)
- ic-healthd serves `/metrics`, `/health` and `/ready` on `:9153`; an empty
`--http-address` / `INCUS_COMPOSE_HEALTHD_HTTP_ADDRESS` disables it. (by
@jochumdev)
- `--metrics` / `INCUS_COMPOSE_HEALTHD_METRICS` flag to record Prometheus
metrics in ic-healthd (defaults to true). (by @jochumdev)
- Running ic-healthd by hand can now present an already-trusted certificate
(`--client-cert` with `--client-key`) or connect as a remote from the Incus
CLI configuration (`--remote` with `--use-remote`). (by @jochumdev)
- Support for Compose Specification lifecycle hooks (`pre_start` and
`post_start`) on services. `pre_start` runs ephemeral init containers sharing
the service's volumes and tmpfs mounts before the main service starts,
supporting `command`, `image`, `user`, `privileged`, `working_dir`,
`environment`, and `per_replica`. Runner containers are deleted on success and
retained stopped on failure for inspection. `post_start` executes commands
non-interactively inside the running service container after startup and
before healthcheck evaluation, supporting `command`, `user`, `privileged`,
`working_dir`, and `environment`. (by @jochumdev)
- Configurable tools volume, mount path, and sleep helper image options on
`client.GlobalClient` (`ClientToolsVolume`, `ClientToolsMount`,
`ClientSleepImage`). (by @jochumdev)
### Changed
- `healthd status` now format output with status, IPv4 and IPv6 addresses, and
optional Prometheus metrics queried directly via `incus port-forward` and
HTTP, add `--port` to override the HTTP port (default: 9153),
`--format <text|json>` (default: `text`), and `--metrics` to include
Prometheus metrics in the report. (by @jochumdev)
- The global network `icompose0` now configures DHCP ranges after creation to
reserve addresses for static IP assignment, and the global DNS sidecar is
assigned a static `.53` address within that reserved range. (by @jochumdev)
- Require incus 7.0.2+ LTS or 7.5+ both unreleased at the time of writing, we
need various recent bugfixes (by @jochumdev)
- ic-healthd is migrated to the `ievent` framework: one chain of plugins
replaces its listener, router and per-project schedulers. The sidecar contract
is unchanged - the same flags, environment variables and status writes, and
`healthd reload` still forces a full resync. (by @jochumdev)
- Refactor the `run` command in `cmd/incus-compose/run.go` to use
`client.Client` abstractions (`c.EnsureTools()`, `c.Config().ToolsMount`,
`c.Config().SleepImage`, and `c.Resource()`) instead of raw Incus API and
`iclient` calls. (by @jochumdev)
- Remove `--healthd-image`, `--sleep-image`, and `--dns-image` CLI flags from
subcommands (`up`, `run`, `pull`, `healthd up`/`down`, `dns up`). Images are
resolved centrally on initialization. (by @jochumdev)
### Removed
- `ic-healthd`: Remove deprecated `user.healthcheck.ignore`
(`shared.HealthIgnoreKey`) tag and check; instance watching is strictly opt-in
via `user.healthcheck.enabled`. (by @jochumdev)
### Fixed
- Bridge network creation on Incus clusters now uses Incus's two-phase
per-member pending network workflow (`?target=<member>`) and co-locates
single-replica services and their volumes onto the connected cluster member
when using a bridge network or member-local storage pool. (by @jochumdev,
#213)
- Bridge network `raw.dnsmasq` now includes `local=/<service>/` alongside
`address=/<service>/<ip>` entries so `AAAA` lookups for IPv4-only service
names are answered locally with `NODATA` instead of being forwarded upstream
and timing out. (by @jlutran, @jochumdev, #236)
- Removing a network ACL while other services change the same OVN network's ACLs
no longer fails with `Failed removing default routes: object not found`. (by
@tallica)
- `logs [SERVICE...]` and foreground `up [SERVICE...]` now filter log output to
the in-scope services (including scaled replicas) and reject unknown service
names. (by @jochumdev, #231)
- On a server without OVN, `up` and the `healthd`/`dns` commands no longer spend
about two seconds detecting that, several times per command, and OVN networks
in projects other than `default` are now found. (by @tallica)
- `build` now works on macOS instead of erroring with "the build command is only
available on linux". (by @tallica)
- `ic-dns` now learns of a bridge network as soon as it is created, so an
instance on a new bridge is enriched with its interfaces on hosts without OVN.
(by @tallica)
- Taking the global lock gives up after five failed attempts with the underlying
error instead of retrying silently for ever. (by @tallica)
- The client's project cache and hooks are safe to use from parallel workers; a
race could drop a cached project or a debug hook. (by @tallica)
- Taking an image lock creates the global project when it does not exist yet, so
a fresh server no longer hangs there. (by @tallica)
- `ic-dns` drops a deleted or renamed network when it happens instead of at the
next sweep. (by @tallica)
## [v1.3.5] - 2026-10-17
### Fixed
- Network `aliases` are now flushed to `raw.dnsmasq` when all services in a
project set `container_name` equal to their service key. Previously, DNS alias
flushing was only triggered during instance address registration, which was
skipped when the container name matched the service name. (by @Tofil,
@jochumdev, #206)
- `down --rmi` now validates its argument (`local` or `all`), preventing
subsequent flags (such as `--volumes`) or invalid values from being silently
consumed. (by @pikeas, @jochumdev, #220)
- `down` now validates service names passed as arguments against declared
services in the Compose project, returning an error if a service does not
exist rather than silently doing nothing. (by @pikeas, @jochumdev, #221)
- Containers on Incus < 7.6 now set `lxc.environment.hooks` via `raw.lxc` to
override `PATH` for LXC hooks running on the host, fixing containers whose
image `PATH` does not match the host's (such as
`ghcr.io/nix-containers/images/*`). (by @jochumdev)
### Added
- Support for direct Incus connections via `--server-url` (`INCUS_SERVER_URL`),
`--client-cert-file` (`INCUS_CLIENT_CERT_FILE`), `--client-key-file`
(`INCUS_CLIENT_KEY_FILE`), and `--server-fingerprint`
(`INCUS_SERVER_FINGERPRINT`). An explicit `--remote` flag takes precedence
over manual connection settings; otherwise, manual connection takes precedence
over `INCUS_REMOTE` or the default Incus remote. (by @jochumdev)
- Support for pass-through bind mounts on remote Incus servers via the inline
volume extension `x-incus-compose.remote: true` or the global
`--allow-remote-mounts` (`INCUS_COMPOSE_ALLOW_REMOTE_MOUNTS`) flag. Both skip
the same-host and local `os.Stat` checks so the source path is resolved on the
remote Incus host. (by @jochumdev)
- External networks referencing an unmanaged host network (such as an unmanaged
bridge `br0` where Incus reports `MANAGED: NO`) are now automatically
translated to `nictype: bridged` (or `physical` / `macvlan`) with
`parent: <name>` on the instance device instead of `network: <name>`, allowing
direct attachment to unmanaged host bridges via standard compose `networks:`
declarations. (by @jochumdev)
### Changed
- Seeded directory bind mounts (`x-incus-compose.seed: true`) now re-seed the
volume on every `up`, overwriting matching files in the target while leaving
other files in the volume untouched. (by @jochumdev)
## [v1.3.4] - 2026-09-17
### Added
- Compose network `ipam.config` pools are now supported. Subnet and gateway
definitions automatically calculate the corresponding Incus `ipv4.address` and
`ipv6.address` gateway CIDRs on custom networks, with explicit `x-incus`
network extensions taking precedence when specified. (by @jochumdev)
### Fixed
- Healthd registration token storage moved from `/run/secrets/token` to
`/secrets/token` for Incus 7.0.1 LTS users, where `/run` tmpfs mounts masked
the token file and caused certificate registration to fail. This temporary
workaround will be removed in v1.4.0. (by @jochumdev, #201)
- `list` now logs the error that ensuring the stack returned, instead of the
literal text `error=err`. (by @pikeas)
- Image builds now clean up their temporary rootfs tar archives in `/tmp` when
the build finishes or fails. Previously, the deletion call on close was
commented out, leaving multi-megabyte `incus-compose-rootfs-*.tar` files
behind in the system temporary directory after every build. (by @jochumdev)
- `up --recreate <service>` now brings back reverse dependencies that were torn
down during the recreate down phase. Previously, `up --recreate` on a service
without `--no-deps` tore down services that depended on it, but the subsequent
ensure phase only scheduled the named service and its forward dependencies,
leaving dependent instances missing. (by @jochumdev, #192)
- Healthd teardown now executes all cleanup stages (stop, delete, and
certificate revocation) even if an earlier stage fails, and `healthd down`
ignores expected benign errors for resources that are not running, not
ensured, not found, or still in use by other replicas. Previously, an error
during the stop phase aborted teardown immediately, leaving healthd containers
or certificates behind. (by @jochumdev, #195)
- `working_dir` on a service now sets the instance's `oci.cwd`; the image's
WORKDIR no longer always wins. `run` without `--workdir` starts there too. (by
@sandroden)
## [v1.3.3] - 2026-09-07
### Fixed
- `up --recreate` no longer reports success when deleting an existing instance
fails. `down()` previously logged deletion errors as warnings and returned
`nil`, causing the subsequent ensure step to accept the still-running instance
and exit 0 without recreating anything. A failed deletion during recreate now
aborts the run with an error, while standalone `down` remains best-effort. (by
@alien43)
- A network attachment with `internal: true` now correctly sets `ipv4.gateway`
and `ipv6.gateway` to `none` even without a static address. Previously,
gateway overrides were only written when a static IP was defined, allowing
services without static addresses on internal networks to keep default routes
and unintended outbound network access. (by @alien43)
- Auto-volume generation now recognizes mount paths covered by custom disk and
tmpfs devices declared in `x-incus-compose.devices`. Previously, mount paths
on extra devices were not populated on the typed structs, making them
invisible to path deduplication. This caused duplicate storage volumes to be
created for image-declared `VOLUME` paths, failing instance creation with
`More than one disk device uses the same path`. (by @alien43)
- Service-name DNS registration (`raw.dnsmasq`) is now skipped on non-bridge
networks such as OVN. Because `raw.dnsmasq` is a bridge-only configuration
option, attempting to write it on OVN networks caused `up`, `start`, and
`stop` commands to fail, preventing projects attached to external OVN networks
from converging. (by @sandroden)
## [v1.3.2] - 2026-08-30
### Changed
- A service whose image ships a Dockerfile `HEALTHCHECK` and whose compose file
says nothing about health now inherits that check, as `docker compose` does.
The service is watched by ic-healthd, so with a restart policy set a check
nobody reviewed can restart it, and a `depends_on: condition: service_healthy`
on it now resolves on the image's check rather than on its run state. Opt out
with `healthcheck: {disable: true}`, which is also honoured now, or with
`user.healthcheck.enabled: "false"` via `x-incus`. The image's own
`HEALTHCHECK NONE` is 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)
### Fixed
- `healthcheck: {disable: true}` no longer enrolls the service for health
checking and writes a `null` test 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 an `exec` lands. 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)
## [v1.3.1] - 2026-08-27
### Fixed
- `cp`, `top` and `events` are actually there. v1.3.0 documented and announced
all three, but the commit adding them never landed in the release. (by
@jochumdev)
## [v1.3.0] - 2026-08-25
### Added
- `services.{name}.x-incus-compose.profiles`: set the instance's full Incus
profile list on create, verbatim - same semantics as `incus launch --profile`,
so a list that omits `default` leaves the instance without it. Until now
nothing in compose could express profile _membership_: `x-incus` merges into
the instance's config map, and `profiles` is 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 carry `user.incus-compose.oneoff=true`: `up`
never reconciles them, `ps` lists them under their service, `down` removes
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; `pull` and `up`
prefetch that helper, which is the only step needing the network, so an
air-gapped site can `run` later. `--sleep-image` / `INCUS_COMPOSE_SLEEP_IMAGE`
/ `x-incus-compose.init` point it at a mirror. A cluster mixing CPU
architectures is not supported. (by @jochumdev)
- `pause`, `unpause`, `kill`, `cp`, `top`, `events` and `port`, matching their
`docker compose` counterparts. A pause also sets `user.healthcheck.stopped`,
since a paused instance answers no healthcheck and ic-healthd would restart
out of the pause. `kill -s` takes only SIGKILL, in docker's three spellings:
the Incus state API carries no signal. `cp` decides which side is the instance
by the name before the colon, so a Windows drive stays local. `top` reports
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 - and `healthd status` prints the shared daemon's
health status key. (by @jochumdev)
- `backup` copies a project's named volumes into a separate `<project>-backup`
project with per-run restore points, where `down --volumes` and
`down --project` cannot reach them. `create`, `list`, `verify`, `restore` and
`delete --keep-last N`; the pool comes from `x-incus-compose.backup.pool`. (by
@ishaan-jindal and @jochumdev)
- Volumes are filled from the image. Every path an image declares as a `VOLUME`
gets 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.d` is no longer empty on the first run.
`x-incus-compose.auto-volumes: false` and `volume: {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`, `s390x` and `riscv64` besides `amd64`
and `arm64`. (by @jochumdev)
### Changed
- `stop`, and with it `restart`, shuts a service down gracefully and kills it
once `--timeout` is up. Both killed outright before, so `--timeout` did
nothing at all. `kill` is the old behaviour under its own name. (by
@jochumdev)
- `--pull always` only re-fetches an image the registry moved, rather than
dropping every registry image and downloading it again per run, and
`up --pull always` recreates 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-resumed` as a start, so `unpause` puts a
service back under watch at once instead of at the next resync. (by
@jochumdev)
- A config or secret setting `uid` without `gid` leaves 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)
### Fixed
- 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/v6` and `arm/v7` resolve 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/passwd` and `/etc/group`, and so may the image's own
`USER`. 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)
- `up` no 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 be `Running`.
(by @jochumdev)
- A health check could fail with `websocket: bad handshake` instead 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 up` stops at the first resource that fails, and no longer leaves a
storage volume behind that every later attempt fails on with `UID mismatch`.
(by @jochumdev)
- An OCI image whose `CMD` is empty is no longer re-read from its registry on
every run. (by @jochumdev)
## [v1.2.0] - 2026-08-14
### Changed
- 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 and `oci.dns.*`. (by @jochumdev)
- The bridge the shared ic-healthd daemon attaches to is now called `icompose0`
rather than `ic-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-start` now returns once the containers are created, as `--detach`
does. 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 --build` now 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 `--recreate` was passed as
well. A service that only consumes an image another service builds is
recreated too; everything else is left alone. (by @jochumdev)
- `--pull always` on 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. Native `incus:` remotes are unaffected. (by @jochumdev)
### Added
- `sysctls:` on a service is mapped to `linux.sysctl.*` on the instance, applied
immediately and kept across a restart. (by @alien43)
- `x-incus-compose.gateway: false` on a service's network attachment allows a
static `ipv4_address`/`ipv6_address` on a network that declares no CIDR. That
combination is otherwise rejected, because the gateway is not known until the
network exists. (by @jochumdev)
### Fixed
- A NIC device carrying `nictype` (`bridged` with a `parent`, say) and no
managed `network:` 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 found` against the image being built; it now names the lock volume and
the project it lives in, and `--debug` reports 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 only `x-incus-compose.network`
had any effect; an explicit `name:` 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 `--timeout` on 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 `ENV`
was dropped on the way in, so a service built from a Dockerfile came up
without the `PATH`, `HOME` and `TERM` the same image pulled from a registry
gets. (by @jochumdev)
## [v1.2.0-rc.3] - 2026-08-07
### Changed
- 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 the `default` project's `default` profile 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 the `default`
project any more. (by @jochumdev)
- `--healthd-network` / `x-incus-compose.healthd.network` now 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. Like `incus`, `workers` and `x-incus`, the first project to bring the
shared daemon up supplies it. (by @jochumdev)
- ic-healthd's default pool sizes are `workers: 128` and `restart-workers: 32`,
up from `32` and `12`. 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.1` or `rc.2`** - those left a daemon in the
> `default` project, and nothing moves it for you. Run
> `incus-compose healthd down --force` **before** upgrading, or afterwards
> delete the `ic-healthd` instance and volume in the `default` project and its
> `ic-healthd-global` certificate. Two daemons watching the same projects
> otherwise both restart the same instances. Releases before `v1.2.0-rc.1` had
> no shared daemon and need nothing.
### Fixed
- `up` no 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.nat` on only for a subnet it picked itself, so a
network given an explicit `ipv4.address` through `x-incus` came up without NAT
and nothing on it could route out. Both now default to `true` for any
non-`internal` network, matching docker; set them in `x-incus` to say
otherwise. Networks that already exist keep the setting they were created
with. (by @jochumdev)
## [v1.2.0-rc.2] - 2026-08-06
### Fixed
- `self-update` installs the build for the machine it runs on. It picked the
first asset in the release instead - `darwin_amd64` for everybody - so on
Linux and Windows it replaced the binary with one that cannot execute. The
broken `self-update` is the one already installed, so 1.0.0 and 1.1.0 users
have to reinstall once with `install.sh`; it works from here on. (by
@jochumdev)
## [v1.2.0-rc.1] - 2026-08-06
### Added
- ic-healthd watches several projects from one event listener, and `--project`
is now optional: without it it watches every project whose config matches
`--project-marker`, by default `user.healthcheck.scope=global`. The flag takes
a `KEY=VALUE` pair now; a bare key still means `KEY=true`. See
[Health Checking](https://docs.incus-compose.org/healthd). (by @jochumdev)
- `x-incus-compose.healthd` gained `scope`, `workers`, `restart-workers` and
`x-incus`. `workers`/`restart-workers` size the daemon's pools; `x-incus` is
Incus instance config for the sidecar, e.g. `limits.cpu`. (by @jochumdev)
- `up` and `healthd up` upgrade 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 like `latest`, and
`git describe` builds - 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 `healthd` sub-commands run without a compose file, acting on the shared
daemon. `incus-compose healthd up` on a bare server creates it before any
project exists; `logs`, `restart`, `reload` and `down` fail with
`no ic-healthd is running` when there is none instead of complaining about a
missing `compose.yaml`. (by @jochumdev)
- `--trace`, a level below `--debug` (which it implies), on both incus-compose
and `ic-healthd run`. The daemon's per-event and per-check lines moved there,
so `--debug` stays readable on a server watching many projects; `--trace` is
what shows the Incus events arriving when a project is not being watched.
incus-compose passes it to the sidecar as `INCUS_COMPOSE_HEALTHD_TRACE`; it
has no level of its own in incus-compose yet. (by @jochumdev)
- `healthd down --force` stops 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](https://docs.incus-compose.org/compose-compatibility#entrypoint-and-command).
(by @jochumdev)
- `--pull never` on `up`, `build` and `pull` never contacts a registry, for
air-gapped use. `pull --policy` is honoured instead of ignored. (by
@jochumdev)
### Changed
- **one ic-healthd for the whole server.** `up` no longer creates a sidecar per
project. It creates a single shared daemon named `ic-healthd` in the Incus
`default` project and marks the project `user.healthcheck.scope=global` so 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 project` or
`x-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](https://docs.incus-compose.org/healthd). (by
@jochumdev)
- **health checking is opt-in.** ic-healthd watches an instance only when it
carries `user.healthcheck.enabled: "true"`; a `healthcheck:` 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, run `up` once to have those enforced
again; it adds the key in place, with no `--recreate` and no downtime.
Projects that use neither need nothing, and containers keep running either
way. See
[Health Checking](https://docs.incus-compose.org/healthd#health-checking-is-opt-in).
(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 Incus `default` project and counts against nothing. (by
@jochumdev)
- `user.healthcheck.status` is written by ic-healthd alone. incus-compose no
longer stamps `starting`/`stopped` on it, so the value always says what a
daemon actually saw: an instance carries no status until one reports, reports
`stopped` while it is down, and `unknown` for good under `up --no-healthd`.
`list` shows those as `Unknown` and `Stopped`. (by @jochumdev)
- the image cache moved from the Incus `default` project to
`incus-compose-cache`. Whatever earlier versions cached in `default` stays
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 `--build` to force a rebuild. (by
@jochumdev)
- `up` without `--detach` now matches `docker compose`: create, start, stream
logs, and `down` on interrupt. (by @jochumdev)
- `config --format=json` keeps the `x-incus` and `x-incus-compose` blocks, which
`docker compose` drops. Parse the JSON rather than diffing it against
docker's. (by @jochumdev)
### Fixed
- ic-healthd reliability: stalled API calls, checker cancellation races, invalid
intervals, `unless-stopped` misread as a deliberate stop, instances re-checked
forever after they stopped, and state lost on an event-listener reconnect. (by
@jochumdev)
- concurrent `up` runs no longer fail creating the same volume, profile or
network. (by @jochumdev)
- `build.dockerfile` is resolved relative to `build.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:` or `secrets:` entry whose target already exists in the image is
written instead of silently skipped. (by @jochumdev)
- a static `ipv4_address`/`ipv6_address` on a network with no explicit address
now fails with an explanation instead of producing a broken NIC. (by
@jochumdev)
- `config --format=yaml` no longer nests the document under a `project:` key.
(by @jochumdev)
- default storage pool detection checks the `default` profile's root device
first. (by @jochumdev)
- a service's `x-incus.raw.dnsmasq` lines are no longer appended twice. (by
@jochumdev)
- pushing directory content into a storage volume no longer closes each file
twice. (by @jochumdev)
## [1.1.0] - 2026-07-31
### Changed
- `build:` now works on every platform instead of Linux/Unix-only: image
building previously unpacked the built OCI image with `umoci` (gated to
`unix && !darwin`, with a "not implemented" stub elsewhere), and now instead
derives the minimal `config.json` needed by Incus's LXC driver
(`Process.Args`/`Cwd`/`User`) straight from `<builder> inspect`, dropping the
`opencontainers/image-spec` and `opencontainers/umoci` dependencies. Builder
detection also no longer shells out to `<builder> version` to distinguish
Podman from Docker; it now checks the resolved binary name, and `buildah` is
tried alongside `podman`/`docker`. (by @jochumdev)
- Published ports (`ports:`) create a proxy device. By default this is a
userspace proxy targeting the container loopback (`nat=false`, connect
`127.0.0.1`); per-port `x-incus-compose.nat: true` opts 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). Requesting `nat` on a server below that floor skips the port with a
warning instead of silently falling back. `nat` was previously auto-enabled on
Incus 7.0+; it's opt-in now because NAT mode doesn't work for host-side
`localhost:<port>` access — it routes to the instance's real address, not the
loopback interface. (by @ishaan-jindal)
- `ic-healthd` is 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 restart` no 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 on
`depends_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 idle `GetInstance` API calls during `up`. (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. Use `build.no_cache: true`
to disable caching. `deleteCached` also no longer aborts before cleaning up
the cache when the source image was already removed. (by @jochumdev)
### Removed
- `x-incus-compose.nat-proxy` extension and all associated post-start device
attachment machinery. Ports are now handled entirely through the standard
`ports:` field. (by @ishaan-jindal)
### Added
- `healthd up` now 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 manual `healthd down` first. `up` shares the same code path, so
a plain `up` also picks up healthd image updates automatically. (by
@jochumdev)
- `services.{name}.configs` / top-level `configs:`: mount config files into the
container, sourced from a file, inline `content`, or an environment variable.
`mode` defaults to `0444` (world-readable); the writable bit is always ignored
per the compose-spec, even if an explicit `mode` is 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
manual `incus remote add` steps. (by @ishaan-jindal)
- Do not ignore healthd in `up --no-deps <service>` it allows script to wait on
the service to be ready. Use `up --no-deps --no-healthd <service>` if you want
the old behaviour. (by @jochumdev)
- `x-incus-compose.healthd.external: true`: the compose-file equivalent of
`up --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}.aliases` on a service's network attachment now registers
extra DNS names for its instance: each alias becomes a
`cname=<alias>,<instance>` record in the network's `raw.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` / `domainname` now map to Incus's `oci.dns.nameservers` /
`oci.dns.search` / `oci.dns.domain` instance config keys, seeding the
container's initial `/etc/resolv.conf`. `dns_opt` has no Incus equivalent and
is not mapped. (by @jochumdev)
### Fixed
- `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.txt` pattern. (by @jochumdev)
- `up --pull=always` and `pull`: 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)
### Internal
- `.golangci.yml`: enabled a much stricter linter set, and fixed the resulting
findings across the codebase. (by @jochumdev)
## [1.0.0] - 2026-07-10
The first stable release! _hooray_
### Changed
- 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-update` got a `--drafts` flag and skips them by default.
## [1.0.0-rc.2] - 2026-07-08
Second release candidate cause of the breaking `user.` -> `user.label.` change
below.
### Changed
- Labels now have a `user.label.` prefix instead `user.` only, to not conflict
with other user settings.
## [1.0.0-rc.1] - 2026-07-07
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.
### Added
- `services.{name}.privileged: true`: run the container privileged
(`security.privileged`).
### Changed
- 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's `oci.entrypoint` as arguments instead of
overwriting it, matching Docker's ENTRYPOINT/CMD semantics.
- `ic-healthd` logs more detail during operations.
- `down --volumes` now deletes volumes while keeping the project; it is no
longer an alias for `--project`. Use `--project` to remove the whole project
(and its volumes).
- `list` includes the ic-healthd sidecar by default; the `--healthd` flag is
replaced by `--no-healthd` to omit it.
### Fixed
- `healthd up` / `healthd down` work with custom networks.
- Windows and macOS builds error cleanly instead of crashing on the umoci
import.
### Removed
- `healthd up --recreate`; recreate the sidecar with `healthd down` followed by
`healthd up`.
### Internal
- CI runs slow tests with a 20m timeout and without parallelism to avoid
overload; tooling paths and changelog links updated; lint fixes.
<details>
<summary><strong>Pre-1.0 beta history</strong> (beta1 through beta.22, 2026-06-01 to 2026-07-06)</summary>
## [1.0.0-beta.22] - 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.
### Added
- `pull` command: 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 numeric `UID` or
`UID:GID` (mapped to `oci.uid` / `oci.gid`).
- `services.{name}.x-incus-compose.devices`: attach raw Incus devices (gpu,
unix-char, ...) verbatim; the required `type` key 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.
### Changed
- `exec` runs as the instance's user/group by default (matching
`docker 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.
## [1.0.0-beta.21] - 2026-07-04
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.
### Added
- `x-incus` extensions now pass through on service networks, service volumes,
and devices, plus direct `tmpfs` on 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 the `INCUS_COMPOSE_HEALTHD_*` prefix, and a `--token` flag was
added.
- Error-severity system: `Clone()` and `IgnoreError()` let commands demote
non-fatal problems to warnings instead of hard failures. `up`/`down`/`start`/
`stop`/`restart` no longer abort on errors that don't matter.
- `StackFailFast()` and `Stack.SetOptions()`.
- Exported `SanitizeProjectName()`.
### Changed
- `exec` uses the native `incus exec` implementation instead of the in-house MVP
terminal (~250 lines removed): better TTY handling and parity with the `incus`
CLI.
- 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()` and `IncusName()`.
### Fixed
- Instance volumes land on the correct storage pool.
- `security.shifted` is 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.
## [1.0.0-beta.20] - 2026-06-30
Internal project/stack refactor plus network-readiness and healthd reliability
fixes.
E2E suite green, ~50% coverage.
### Added
- 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.
### Changed
- Reworked the ordering logic for `up`/`down`/`start`/`stop`, with and without
dependencies. Deliberate asymmetry: `up`/`down` follow `depends_on` by default
(`--no-deps` limits to the named service); `start`/`stop`/`restart` act on the
named service only (`--with-deps` makes them follow `depends_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`.
### Fixed
- DNS update retries once on an ETag mismatch (concurrent-update race).
- `user.healthchecking.stopped` updates go through a cleaner path; the hacky
PATCH workaround is gone.
### Removed
- deb/rpm/apk packages. Releases now ship the tarball/binary and install script
only.
## [1.0.0-beta.19] - 2026-06-29
Mostly CLI and healthd fixes, plus event-driven log following.
### Added
- 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).
### Changed
- `down --project` now deletes all resources (instances, networks, volumes, and
the healthd sidecar) instead of relying on incus to do so.
- `--debug` no longer shows progress bars (they interfered with debug output).
- DNS watcher waits up to 5s after a dnsmasq restart before starting the next
instance.
### Fixed
- healthd: restart counting during the start period, instance tracking after
cancellation, and the `healthd up`/`healthd down` lifecycle (#5).
### Removed
- Automatic retry on client operations.
- The unnecessary `--with-deps` flag from `logs`.
### Docs
- Added a Terms section (#4); updated example healthchecks with `start_*`
directives; immich example now waits for DNS readiness and drops tini.
## [1.0.0-beta.18] - 2026-06-28
### Changed
- **Breaking:** renamed the `--project-directory` shorthand from `-pd` to `-P`.
- **Breaking:** `core.https_address` is 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 `--workers` from 10 back to 4 to avoid storage IO
contention on cold-cache / large-image starts.
- Use the non-`v` version for the healthd image while keeping the `v` prefix for
incus-compose itself.
### Fixed
- Retry various client/CLI operations and tune the default timeouts; increased
the delay between start/stop retries.
- `up --recreate` no longer recreates networks.
- `ic-healthd` no longer shows up as an orphan in `ps` output.
- healthd always restarts checkers on reload so new settings take effect.
- Silenced two noisy debug logs.
## [1.0.0-beta.17] - 2026-06-27
Hotfix on top of beta.16, focused on the health-check sidecar.
### Added
- Opt-out of volume shifting (`security.shifted`) for cases where matching IDs
inside the container don't matter.
### Fixed
- `ic-healthd` now runs inside the project stack and attaches to the project's
own network (the project default unless overridden via
`x-incus-compose.healthd.network`), ensured just before regular instances.
Network extra options are no longer lost.
## [1.0.0-beta.16] - 2026-06-26
Requires updating via the install script (version format changed to `beta.XX`
for a correct semver version).
### Added
- `list` now has a separate `HEALTH` column instead of appending health to
`STATUS` (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`/`start` wait for the healthcheck to report healthy after starting an
instance that defines one (polled every 500ms, bounded by `--timeout`), making
`depends_on: service_healthy` reliable.
- `ic-healthd` reports its own status (healthy on start, unhealthy on shutdown)
and locates the daemon instance via a `user.healthcheck.daemon` marker.
- New `--healthd-incus` flag / `INCUS_COMPOSE_HEALTHD_INCUS` env 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.healthd` extension (`incus` API URL and
`network` as `<project>:<network>` or a plain bridge name; both default to the
project's own network and the connection's port).
### Changed
- **Breaking:** compose now defaults to incus listening on all interfaces; set
`INCUS_COMPOSE_HEALTHD_INCUS` to override.
- `up` reconciles the service count in both directions, matching
`docker compose up`. Instances above the desired count (`deploy.replicas` or
`--scale`) are torn down, highest index first. A manual `--scale N` applies
only to that invocation (#12).
- Default `--timeout` raised from 10s to 1 minute.
### Removed
- **Breaking:** the defunct `x-incus-compose.network-profile` extension
(replaced by `x-incus-compose.healthd`).
## [1.0.0-beta15] - 2026-06-23
### Added
- `self-update` command: 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).
### Fixed
- `.incus.yaml` overlay loading: `docker-compose.incus.yaml` and
`compose.incus.yaml` overlays were not loaded correctly (#6).
- Progress display and error rendering improvements (#7).
- healthd retry calculation: `retries = start_period / start_interval`.
- Small client-package cleanups.
### Docs
- Improved healthd documentation.
## [1.0.0-beta14] - 2026-06-22
### Added
- Network project/profile support: `x-incus-compose.network.project` and
`x-incus-compose.network.profile` control which Incus project and profile
healthd uses.
### Changed
- **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: true`
on the volume to restore copying files into the instance.
### Fixed
- Client connection stability: fixed several data races (reused `ProtocolIncus`,
`noColor` context var, random string generation).
- Network profile fallback: use `devices.eth0.parent` when
`devices.eth0.network` isn't available in a profile.
- More robust healthd discovery (`FindHealthd`).
- Switched to `errors.As()` for proper error unwrapping.
### Internal
- Removed all remaining testify/suite usage; refactored `serviceToInstance()`
into smaller helpers; split `project/project.go` into smaller files; removed
dangling test fixtures; updated snapshots for the new bind-mount behavior.
## [1.0.0-beta13] - 2026-06-17
### Changed
- incus-compose is now part of the **lxc** organization on GitHub.
- The `main` branch entered a feature freeze ahead of 1.0.0.
### Fixed
- Do not assume the availability of `incusbr0`.
## [1.0.0-beta12] - 2026-06-13
### Added
- `--image-cache` global flag (`INCUS_COMPOSE_IMAGE_CACHE`) to point the image
cache at a different Incus project (default: `default`).
- `--rmi` / `--images` on `down` to remove project images on teardown, matching
docker compose behaviour.
- Extra storage volume config support via `x-incus-compose`.
### Fixed
- `--with-deps` scoping: `up`/`down` follow `depends_on` automatically; all
other commands require an explicit `--with-deps`.
- Healthd is now skipped when no services require it.
- Build image name corrected to `localhost/<service>`.
## [1.0.0-beta11] - 2026-06-11
Moved to 1.0.0 from 0.0.1 (1.0.0 will be the final version). Mostly a
health/dependency/lifecycle hardening release.
### Added
- Healthy-dependency support: `up` wires `service_healthy` dependencies into
start ordering/waiting; new `--dependency-timeout` flag.
- Healthd expanded and rewritten: now runs when services use restart policies or
are depended on via `service_healthy`, not only for an explicit healthcheck.
Supports `start_period`, `start_interval`, `interval`, `timeout`, and
`retries`; health state standardized via `user.healthcheck.status`.
- `shm_size` maps to a `/dev/shm` tmpfs device.
- `container_name` support: used as the Incus instance name; scaled services
become `container_name-1`, `container_name-2`, etc.
- Added `examples/many-dependencies` and `examples/wikijs`; moved
`test/fixtures/immich` to `examples/immich`; timestamped test logs under
`test/logs/`; added `just test-slow`.
### Changed
- **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 plain `ic-healthd` need 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.
### Fixed
- `exec` now targets the requested service instead of possibly choosing the
wrong instance from the stack.
- `list`/`ps` output 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 `0` means "do not wait", so the internal default now maps to `-1`
where needed.
- Storage volumes: better shifted-volume validation, delayed from `Ensure()` to
`Start()` 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.
## [0.0.1-beta10] - 2026-06-08
Making Compose workflows more complete: local image builds, better progress
feedback, and improved healthcheck restart behavior.
### Added
- Compose `build:` support. New commands and flags: `incus-compose build`,
`incus-compose build <svc...>`, `up --build`, `up --no-build`. See
`docs/build.md`.
- Progress output for all operations in the CLI.
### Changed
- **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-healthd` restart backoff (5s -> 10s -> 20s -> 40s -> 60s max) avoids tight
restart loops for unhealthy services.
- `restart: unless-stopped` handled more correctly: `stop` marks
`user.stopped=true`, `start` clears it, and `ic-healthd` skips automatic
restarts while a service was intentionally stopped.
### Fixed
- 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.
## [0.0.1-beta9] - 2026-06-07
Major image-handling rework, plus volume/bind-mount and healthd changes.
### Changed
- 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.) via `UpdateImage`; 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: `GetImageServer` and `EnsureProject` moved
from `newImage()` into `Ensure()`, so no network connections happen during the
configuration phase (fixing CI slowness).
- Resource deduplication: `ResourceStore.Get` now compares by `IncusName()`, so
normalized references (`docker.io/nginx:alpine` vs
`docker.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 with `HostPath` seeding on create.
`PostDevices` and `ActionPostEnsure` are removed; volumes live in `Devices`
and are ensured before `CreateInstanceFromImage`.
- Healthd resource removed: the `client.Healthd` wrapper is replaced by
`healthdUp`/`healthdDown` helpers in `cmd/incus-compose/healthd.go`.
`KindHealthd`, `HealthdConfig`, and `resource_healthd.go` are deleted.
- Healthd instance is prefixed with the project name to prevent cross-project
collisions.
### Fixed
- Skip token registration when a cert already exists (prevents repeated
re-registration on `ic-healthd` restart).
- `down` network-listing failure during `--project` delete is demoted to a
warning; a nil-check prevents a panic when listing fails.
### Internal
- justfile version tags gained `--long --dirty` for healthd builds.
### Breaking
- Removed the project name from storage volume names (manual migration required;
note beta10 renamed volumes again to `vol-{name}`).
## [0.0.1-beta8] - 2026-06-06
### Added
- `list --healthd`: opt-in flag to include the `ic-healthd` sidecar 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 as `user.image_alias` at creation, resolved from
`volatile.base_image` as a fallback).
- healthd resource limits: the sidecar is capped at 1 CPU and 50 MB RAM by
default (required for project-wide limits).
- `ic-healthd` now compiles in and prints its version.
- `oci-registry-cache` promoted to a standalone helper project.
### Changed
- `healthd restart` works as intended: kill a service and healthd brings it
back.
- **Breaking:** `--no-pull` replaced by `--pull` (string flag) for docker
compose compatibility.
### Fixed
- `down` no 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.
## [0.0.1-beta7] - 2026-06-05
### Added
- `incus-compose healthd` subcommand group for direct sidecar management: `logs`
(stream), `reload` (reload health-check config), `restart`, `up` (recreate,
`--recreate` supported), and `down`.
- 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.
### Fixed
- 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-healthd` now appears in `list`/`ps` output.
- Hardcoded default storage pool: `ic-healthd` resources now use the client's
configured `DefaultStoragePool` instead of always `"default"`.
- `up --recreate` on a healthd container no longer loses `--incus`/`--project`
OCI entrypoint flags; `ResourceStore.Remove()` is now called on every
`Delete()`.
## [0.0.1-beta5] / [0.0.1-beta6] - 2026-06-04
### Added
- Automatic loading of a `compose.incus.yaml` override file when present next to
the main Compose file, keeping upstream Docker Compose files unchanged while
adding Incus-specific configuration in a separate file.
### Changed
- The `ic-healthd` image now uses `busybox:glibc` instead of `scratch`.
## [0.0.1-beta4] - 2026-06-04
### Added
- `x-incus` options: 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-profile` support (disables a per-project
default network/bridge).
- healthd reload on service changes.
### Fixed
- Network creation race that could cause dnsmasq failures in CI (avoids
immediately updating a newly-created network before the old dnsmasq released
its socket).
- `down` now deletes compose-managed networks when the project is brought down,
fixing dangling networks.
- `up --no-pull` is now respected correctly (also ~2x faster test runs, 3min
from 6min).
### Testing
- Test coverage +17% (35% -> 52%).
## [0.0.1-beta3] - 2026-06-03
### Added
- Kernel-mode NAT proxy for port proxies via `x-incus.nat-proxy` (#30).
- DHCP ranges and static IPv4 / IPv6 addresses on network creation.
### Fixed
- Healthd status in `ps`/`list`.
- Scaling now prunes dangling instances when `up --scale` lowers the count
(#34).
- `logs` omits old logs then follows when `--follow` is set.
- Small fixes to keep CI green.
## [0.0.1-beta1] - 2026-06-01
Initial public beta. A ground-up Docker Compose workflow for Incus, inspired by
Brian Ketelsen's proof-of-concept.
### Added
- Familiar commands: `up`, `down`, `start`, `stop`, `restart`, `list` (and
`ps`), `logs`, `exec`, `config`, plus `build`, `healthd`, `incus`
(pass-through), and `self-update`.
- Compose project parsing via compose-go, with automatic `compose.incus.yaml`
overrides and `x-incus` / `x-incus-compose` extensions 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_healthy` ordering
via the `ic-healthd` sidecar.
- Service scaling with `up --scale` and 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.
</details>