Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama develop · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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>