Incus-compose - the missing docker-compose for incus

[error: in an operation: Failed to setup device mount "bind-media-zim": idmapping abilities are required but aren't supported on system]

For now we always shift mounts into the container. You could set security.shifted=false but will make your mount readonly in the container and … I have to change the code a little to make that possible.

That’s wanted healthd should live forever! You can get rid of it with incus-compose down --project but that will remove everything.

I have something funky for you: From LXC to Incus mounting with rbind

This could work with incus-compose completly untested though.

compose.yaml

services:
  kiwix:
    image: ghcr.io/kiwix/kiwix-tools:latest
    container_name: kiwix
    ports:
      - 8980:8980
    volumes:
      - ./data:/data
    command: "kiwix-serve --library --port 8980 -L 6 --monitorLibrary /data/library.xml"
    restart: unless-stopped

compose.incus.yaml

services:
  kiwix:
    x-incus:
      raw.lxc: |-
        lxc.mount.entry = /media/zim/ media/zim/ defaults,rbind,create=dir,idmap=container 0 0

There’s no need to split those 2, i just like it to see incus-compose specific changes.

Thanks so much for the suggestion. With the *incus.yml override file, incus-compose up is erroring with start instance kiwix \[error: in an operation: Failed to run: /opt/incus/bin/incusd forkstart kiwix_kiwix /var/lib/incus/containers /run/incus/kiwix_kiwix/lxc.conf /var/log/incus/kiwix_kiwix: exit status 1\]

Did some experimenting and this ‘cheat’ appeared to do something.

  • incus-compose up without the bind mount defined in the .yml
  • incus config device add kiwix zim disk source=/media/zim path=/media/zim --project kiwix
  • incus-compose restart kiwix

The /media/zim mount and it’s contents now appears in the file system. The content is served.

This mapping survives a restart, stop/start, stop/up but not after a down.

Anway, it’s all good. I was just testing to see which existing docker containers’ docker-compose.yml can be moved to incus-compose as-is. There are some that have bind mounted network shares.
In this specific case, I was able to get it going with conventional incus launching an OCI instance, then adding a disk device.

Also wanted to confirm that beta16 finds the connected host bridge IP address without needing to specify –healthd-network.
Thanks!

I’ll test this and provide a raw.lxc that works.

Do you care about the content having wrong IDs inside the container (not shifted) if not I could make it easy for people to not shift a volume so you don’t need to add a device.

beta.16 Intodruced a bug with creating networks, hot fix will be out soon.

Blurry, thanks for all the testing, that helps! Do you want to be credited as tester?

In this case, it woudn’t matter that the IDs don’t match up. For my limited scenarios, I can mess around with uid/gid when mounting CIFS shares if need be. But I need to think about it some more for the general case.

My hope is to ultimately to go 100% Incus.

      raw.lxc: |-
        lxc.mount.entry = /run/media/r3j0/Ventoy data/ none rbind,create=dir,idmap=container 0 0

That one is tested :slight_smile: the none is extra.

Thanks again. Still generating an error. Please let me know if we should move this to a seperate forum thread instead of flooding your announcement thread.

$ incus version
Client version: 7.2
Server version: 7.2

$ incus-compose version
incus-compose version 1.0.0-beta.16
$ incus-compose up
22:43 ERR Starting resources project=kiwix error="in an operation: instance(kiwix): Failed to run: /opt/incus/bin/incusd forklxc kiwix_kiwix /var/lib/incus/containers /run/incus/kiwix_kiwix/lxc.conf /var/log/incus/kiwix_kiwix: exit status 1"
ensure   network            default                                [done]
ensure   image              ghcr.io/lxc/incus-compose/ic-healthd:~ [done]
ensure   storage-volume     ic-healthd                             [done]
ensure   instance           kiwix-ic-healthd                       [done]
start    instance           kiwix-ic-healthd                       [done]
ensure   image              ghcr.io/kiwix/kiwix-tools:latest       [done]
ensure   instance           kiwix                                  [done]
start    instance           kiwix                                  [error: in an operation: Failed to run: /opt/incus/bin/incusd forklxc kiwix_kiwix /var/lib/incus/containers /run/incus/kiwix_kiwix/lxc.conf /var/log/incus/kiwix_kiwix: exit status 1]
$ incus-compose list
KIND      NAME     INCUSNAME      IMAGE                             STATUS   HEALTH   ADDRESSES
network   default  kiwix-default                                    Exists
instance  kiwix    kiwix          ghcr.io/kiwix/kiwix-tools:latest  Stopped  Stopped

$ incus-compose ps
NAME      SERVICE   INCUS_NAME        IMAGE  STATUS   ADDRESSES
<orphan>  <orphan>  kiwix-ic-healthd         Running
$ sudo tail /var/log/incus/kiwix_kiwix/lxc.log
[...]
lxc kiwix_kiwix 20260627024423.645 WARN     cgfsng - ../src/lxc/cgroups/cgfsng.c:cgroup_tree_create:550 - File exists - Failed to create monitor cgroup 10(lxc.monitor.kiwix_kiwix-11)
lxc kiwix_kiwix 20260627024423.685 ERROR    utils - ../src/lxc/utils.c:safe_mount:1332 - Invalid argument - Failed to mount "none" onto "/opt/incus/lib/lxc/rootfs/run"
lxc kiwix_kiwix 20260627024423.685 ERROR    conf - ../src/lxc/conf.c:lxc_idmapped_mounts_parent:3569 - Invalid argument - Failed to idmap detached recursive mount 12 to 24
lxc kiwix_kiwix 20260627024423.685 ERROR    start - ../src/lxc/start.c:lxc_spawn:2072 - Failed to setup mount entries
lxc kiwix_kiwix 20260627024423.692 ERROR    network - ../src/lxc/network.c:lxc_netdev_restore_altnames:1422 - Invalid argument - Failed to get altnames for interface "vethff14b28b"
lxc kiwix_kiwix 20260627024423.692 WARN     network - ../src/lxc/network.c:lxc_delete_network_priv:3940 - Failed to restore altnames for interface with index 0 and initial name "vethff14b28b"
lxc kiwix_kiwix 20260627024423.692 WARN     network - ../src/lxc/network.c:lxc_delete_network_priv:3945 - Failed to rename interface with index 0 from "physqKKSqm" to its initial name "vethff14b28b"
lxc kiwix_kiwix 20260627024423.692 ERROR    lxccontainer - ../src/lxc/lxccontainer.c:wait_on_daemonized_start:837 - Received container state "ABORTING" instead of "RUNNING"
lxc kiwix_kiwix 20260627024423.693 ERROR    start - ../src/lxc/start.c:__lxc_start:2288 - Failed to spawn container "kiwix_kiwix"
lxc kiwix_kiwix 20260627024423.693 WARN     start - ../src/lxc/start.c:lxc_abort:1229 - No such process - Failed to send SIGKILL via pidfd 17 for process 3737291
lxc 20260627024423.752 ERROR    af_unix - ../src/lxc/af_unix.c:lxc_abstract_unix_recv_fds_iov:218 - Connection reset by peer - Failed to receive response
lxc 20260627024423.752 ERROR    commands - ../src/lxc/commands.c:lxc_cmd_rsp_recv_fds:128 - Failed to receive file descriptors for command "get_init_pid"
$ sudo cat /run/incus/kiwix_kiwix/lxc.conf
lxc.log.file = /var/log/incus/kiwix_kiwix/lxc.log
lxc.log.level = warn
lxc.console.buffer.size = auto
lxc.console.size = auto
lxc.console.logfile = /var/log/incus/kiwix_kiwix/console.log
lxc.sched.core = 1
lxc.mount.auto = proc:rw sys:rw cgroup:rw:force
lxc.autodev = 1
lxc.pty.max = 1024
lxc.mount.entry = /dev/fuse dev/fuse none bind,create=file,optional 0 0
lxc.mount.entry = /dev/net/tun dev/net/tun none bind,create=file,optional 0 0
lxc.mount.entry = /sys/firmware/efi/efivars sys/firmware/efi/efivars none rbind,create=dir,optional 0 0
lxc.mount.entry = /sys/fs/fuse/connections sys/fs/fuse/connections none rbind,create=dir,optional 0 0
lxc.mount.entry = /sys/fs/pstore sys/fs/pstore none rbind,create=dir,optional 0 0
lxc.mount.entry = /sys/kernel/config sys/kernel/config none rbind,create=dir,optional 0 0
lxc.mount.entry = /sys/kernel/debug sys/kernel/debug none rbind,create=dir,optional 0 0
lxc.mount.entry = /sys/kernel/security sys/kernel/security none rbind,create=dir,optional 0 0
lxc.mount.entry = /sys/kernel/tracing sys/kernel/tracing none rbind,create=dir,optional 0 0
lxc.mount.entry = /dev/zfs dev/zfs none bind,create=file,optional 0 0
lxc.mount.entry = /dev/mqueue dev/mqueue none rbind,create=dir,optional 0 0
lxc.include = /opt/incus/share/lxc/config//common.conf.d/
lxc.arch = linux64
lxc.hook.version = 1
lxc.hook.pre-start = /proc/3724863/exe callhook /var/lib/incus 'kiwix' 'kiwix' start
lxc.hook.stop = /opt/incus/bin/incusd callhook /var/lib/incus 'kiwix' 'kiwix' stopns
lxc.hook.post-stop = /opt/incus/bin/incusd callhook /var/lib/incus 'kiwix' 'kiwix' stop
lxc.tty.max = 0
lxc.uts.name = kiwix
lxc.mount.entry = /var/lib/incus/guestapi dev/incus none bind,create=dir 0 0
lxc.apparmor.profile = incus-kiwix_kiwix_</var/lib/incus>//&:incus-kiwix_kiwix_<var-lib-incus>:
lxc.seccomp.profile = /var/lib/incus/security/seccomp/kiwix_kiwix
lxc.idmap = u 0 1000000 1000000000
lxc.idmap = g 0 1000000 1000000000
lxc.environment = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
lxc.environment = "HOME=/root"
lxc.environment = "TERM=xterm"
lxc.environment = CREDENTIALS_DIRECTORY=/dev/.incus-systemd-credentials
lxc.mount.entry = /var/lib/incus/containers/kiwix_kiwix/credentials dev/.incus-systemd-credentials none bind,ro,create=dir 0 0
lxc.mount.auto = shmounts:/var/lib/incus/shmounts/kiwix_kiwix:/dev/.incus-mounts
lxc.net.0.type = phys
lxc.net.0.name = eth0
lxc.net.0.flags = up
lxc.net.0.link = vethff14b28b
lxc.net.0.hwaddr = 10:66:6a:10:e8:f1
lxc.rootfs.path = dir:/var/lib/incus/storage-pools/default/containers/kiwix_kiwix/rootfs
lxc.rootfs.options = idmap=container
lxc.mount.entry = /var/lib/incus/devices/kiwix_kiwix/disk.bind--mnt--storage--oci--containers--kiwix--data.data data none bind,create=dir,idmap=container 0 0
lxc.sysctl.net.ipv4.ping_group_range = 0 999999999
lxc.sysctl.net.ipv4.ip_unprivileged_port_start = 0
lxc.execute.cmd = kiwix-serve --library --port 8980 -L 6 --monitorLibrary /data/library.xml
lxc.init.cwd = /
lxc.init.uid = 0
lxc.init.gid = 0
lxc.mount.entry = shm dev/shm tmpfs nosuid,noexec,nodev,mode=1777,size=65536k,create=dir 0 0
lxc.mount.entry = none run tmpfs none,mode=755,optional
lxc.mount.entry = /var/lib/incus/containers/kiwix_kiwix/network/hosts etc/hosts none bind,create=file
lxc.mount.entry = /var/lib/incus/containers/kiwix_kiwix/network/hostname etc/hostname none bind,create=file
lxc.mount.entry = /var/lib/incus/containers/kiwix_kiwix/network/resolv.conf etc/resolv.conf none bind,create=file
lxc.hook.start-host = /proc/3724863/exe forknet dhcp /var/lib/incus/containers/kiwix_kiwix/network /var/log/incus/kiwix_kiwix/forknet-dhcp.log
lxc.mount.entry = /media/zim media/zim none rbind,create=dir,idmap=container 0 0

That is the error.

Yeah, lets do that. Please also see if you can create the device with raw.lxc manualy.

1.0.0-beta.17 is out. It’s a hotfix on top of 1.0.0-beta.16 with a single, focused change to the health-check sidecar.

Fix: ic-healthd uses the project network

Previously the ic-healthd sidecar spun up the network on its own, when the network had extra options they would get have lost.

Now the sidecar runs inside the project stack and attaches to the project’s own network (the project default network unless you override it via x-incus-compose.healthd.network). It’s ensured just before your regular instances, so the network it needs is already in place.

Upgrade

From 1.0.0-beta.16

incus-compose self-update

Before:

curl -sSfL https://raw.githubusercontent.com/lxc/incus-compose/main/install.sh | sh -s -- -b ~/.local/bin

I sneaked a feature into beta.17 - incus-compose/docs/compose-compatibility.md at main · lxc/incus-compose · GitHub :slight_smile:

If you realy don’t care about shifting that will help.

1 Like

Very cool, that works! Thank you.:folded_hands:

1 Like

v1.0.0-beta.18

To update from 1.0.0-beta.16:

incus-compose self-update

or update with the script:

curl -sSfL https://raw.githubusercontent.com/lxc/incus-compose/main/install.sh | sh -s -- -b ~/.local/bin

Highlights / Breaking

  • CLI: renamed the --project-directory shorthand from -pd to -P.
  • Config: core.https_address is now required (incus-compose needs the
    server reachable over the network for image caching); the CLI now warns when
    connecting over a unix socket.
  • Defaults: lowered the default --workers from 10 back to 4 to avoid
    storage IO contention on cold-cache / large-image starts.

Features

  • Use the non-v version for the healthd image while keeping the v prefix
    for incus-compose itself.
  • Warn the user when connecting via a unix socket.

Fixes

  • client/cli: retry various operations and tune the default timeouts.
  • client: increase the delay between start/stop retries.
  • cli: up --recreate no longer recreates networks.
  • cli: ic-healthd no longer shows up as an orphan in ps output.
  • healthd: always restart checkers on reload so new settings take effect.
  • client: silenced two noisy debug logs.

Internal / Docs

  • CI: improved the GitHub workflows; updated the github-runner doc.
  • Added example-based tests and more volume tests.
  • Minor client optimizations.
1 Like

See

It’s been some work to make these all working on the “slower” CI, on my machine everything was working great with high concurrency the CI disliked that :slight_smile:

I’m running:

In production since a while, will run hugo soon as well instead my custom build.

1.0 is close — aiming for a week or two, gated on testing.

On the “feature freeze” in the beta13 announcement: it meant no big refactors, and I held to that. The biggest thing I’m holding back is a healthd rewrite — moving it from polling to events/push — which lands post-1.0. Two small features (seed, security.shifted) slipped in because testing showed they were needed to actually use the tool.

1 Like

Does incus-compose logs keep track of lines it has already displayed? Each subsequent invocation does not show any prior lines. I was expecting it to display from the start, like docker compose logs

Trying to figure out why another stack that I am testing is semi-failing. One of the units fail/stop. The stack runs and stays up inside bare metal or incus VM docker compose.

Ye it doesn’t thats a limitation of incus. It helps to run a created instance manualy:

incus-compose incus start --console web-1

Thank you for the logging hint. That helped me narrow the probable cause as the instance failing to make an outbound TCP connection.

I don’t know why, as the stack works with Docker bare-metal, Docker in an Incus VM and nested Docker in an Incus container.

Ohh that could be dnsmasq. To make DNS working like in docker-compose I have to alias database-1 to database in dnsmasq, that requires a restart of dnsmasq and makes DNS failing for a second or 2 on start.

Containers which dont retry fail then - immich has some examples for that. I’ll find a proper fix for that.

You could run up twice for now maybe with --no-healthd.

Thank you for all the testing, would be lovely if you report your stacks here.

Last line in the log.
"level":"fatal","error":"network Error : dial tcp 123.123.123.123:1883: i/o timeout","time":"2026-06-28T22:35:53-04:00","message":"failed to connect to mqtt broker"}

I’ll see about cleaning up the stack and forwarding to you. Tomorrow, most likely. Thank you.

Thats a public ip, do you want to connect to that one in the container?

Hmm do you use my networking feature as in:

networks:
  default:
    x-incus:
      ipv4.nat: "true"
      ipv4.address: 10.131.32.1/24

I once had to add that “ipv4.nat” to allow the container to reach the internet.