Services

Labels

Compose labels are stored on the instance as user.label.<key> config keys. Both the map and list forms work:

services:
  app:
    image: docker.io/nginx:alpine
    labels:
      caddy: whoami.example.com
      caddy.reverse_proxy: "{{upstreams 80}}"
  api:
    image: docker.io/nginx:alpine
    labels:
      - "traefik.http.routers.api.rule=Host(`api.example.com`)"

becomes:

config:
  user.label.caddy: whoami.example.com
  user.label.caddy.reverse_proxy: "{{upstreams 80}}"
  user.label.traefik.http.routers.api.rule: "Host(`api.example.com`)"

Two labels are always added:

Key Value
user.label.incus-compose.project the compose project name
user.label.incus-compose.service the compose service name

Read them back with the incus passthrough:

incus-compose incus config get app-1 user.label.caddy

Service discovery - the user.label. prefix keeps compose labels out of the user.* namespace incus-compose uses for its own keys, and mirrors the label conventions of reverse proxies and DNS managers:

None of these tools support incus-compose yet: they discover services over the Docker socket, not the Incus API. incus-compose only exposes the labels as user.label.* instance config; consuming them needs an Incus-aware discovery integration.

Changed in 1.0.0-rc.2: labels moved from user.<key> to user.label.<key>, and the incus-compose.project / incus-compose.service labels were added.

User

The user attribute overrides the user the container process runs as, mapping to the image's oci.uid / oci.gid:

services:
  web:
    image: docker.io/nginx:alpine
    user: "1000:1001" # UID:GID; the GID is optional

Either side may be a number or a name the image's own /etc/passwd and /etc/group define, so 1000, 1000:root, nobody and netbox:root all work:

services:
  netbox:
    image: docker.io/netboxcommunity/netbox:v4.6-5.0.2
    user: "netbox:root"

A name the image does not define is an error, not a fall back to root. The same resolution applies to the image's own USER, so an image built with USER nginx runs as nginx without a user: of its own.

A name with no group takes that user's own group, as login would - busybox runs nobody as 65534:65534. A number with no group keeps GID 0, because resolving it would mean reading the image for every service that sets user:.

An image has no file API, so resolving a name starts a stopped instance from the image and reads it over SFTP. That is one instance per image per command, shared by every service using that image and removed when the command ends. A user: that is numeric on both sides never reads the image at all.

The Compose Specification only says user "overrides the user used to run the container process" and does not document a value format. The UID:GID form is Docker's convention, which we follow.

Since: 1.0.0-beta.22

Changed in 1.3.0: user and group names resolve against the image; only numbers were accepted before.

Entrypoint and Command

entrypoint: behaves as the compose spec describes: it replaces the image's entrypoint, and the image's default command is discarded, so the container runs exactly entrypoint: followed by command:.

services:
  web:
    image: docker.io/library/busybox:glibc
    entrypoint: ["httpd", "-f", "-v", "-p", "8080", "-h", "/www"]
entrypoint: command: The container runs
set unset entrypoint
set set entrypoint + command
set [] entrypoint
[] set command
[] unset rejected - nothing to run
unset set image entrypoint + command

command: on its own replaces the image's CMD and keeps its ENTRYPOINT, as Docker does. An image with ENTRYPOINT ["caddy"] and CMD ["run"] plus command: ["version"] runs caddy version.

Incus cannot answer which part of an image's argv was the ENTRYPOINT: it reports the two already concatenated. incus-compose reads the split from the image's own config in the registry instead, before the image is pulled, and keeps it in the image's oci.entrypoint and oci.cmd properties. A locally built image gets the same from the builder.

Reading that config is the one thing incus-compose asks of a registry directly rather than pointing the server at it, so a client that cannot reach the registry its server pulls from logs a warning and stores no split. command: then runs on its own, without the image's entrypoint in front of it - set entrypoint: to say exactly what should run, which needs nothing from the image.

Since: v1.3.0 - until v1.2.0 command: appended to the image's whole argv, so the example above ran caddy run version.

DNS

dns, dns_search, and domainname map to Incus's oci.dns.* instance config keys, which seed the container's initial /etc/resolv.conf:

services:
  web:
    image: docker.io/nginx:alpine
    dns:
      - 8.8.8.8
      - 1.1.1.1
    dns_search:
      - example.com
    domainname: example.com

becomes:

config:
  oci.dns.nameservers: 8.8.8.8,1.1.1.1
  oci.dns.search: example.com
  oci.dns.domain: example.com

Each key is only set when the corresponding compose field is non-empty. dns_opt has no Incus equivalent and is not mapped.

Since: v1.1.0

Sysctls

sysctls sets kernel parameters on the instance, mapping each key to linux.sysctl.<key>. Both the map and list forms work:

services:
  vpn:
    image: docker.io/nginx:alpine
    sysctls:
      net.ipv4.conf.all.src_valid_mark: 1
      net.ipv6.conf.all.disable_ipv6: 0
  web:
    image: docker.io/nginx:alpine
    sysctls:
      - net.core.somaxconn=1024

becomes:

config:
  linux.sysctl.net.ipv4.conf.all.src_valid_mark: "1"
  linux.sysctl.net.ipv6.conf.all.disable_ipv6: "0"
  linux.sysctl.net.core.somaxconn: "1024"

The value applies when the instance starts and survives a restart, on both privileged and unprivileged containers. Which parameters are writable from inside an unprivileged container is Incus's business, not ours: a key the kernel refuses in that namespace fails at start rather than being ignored.

Since: v1.2.0

Environment

The bare ${VAR} form, with no :-/? operator, does not fail on a missing or empty value - it silently interpolates to an empty string. Only the ${VAR?message} form hard-fails.

Build

See Builds for supported options, builder selection, and platform handling.

Health Checks

Supported via the ic-healthd sidecar. See Health Checking for full details, including config keys, defaults, security model, and healthd management commands.

The healthcheck status (starting, healthy, unhealthy) is reported in the Status column of incus-compose list and incus-compose ps when healthchecks are configured.

Resource Limits

deploy.resources is not mapped. Use x-incus to set Incus instance limits directly:

services:
  app:
    x-incus:
      limits.cpu: "1"
      limits.memory: 512MiB

Any Incus instance config key is accepted. See Architecture for full details.

Restart Policies

Restart policies map to Incus boot configuration:

Compose restart Incus Config
no (default) boot.autostart=false
always boot.autostart=true
on-failure boot.autostart=true, boot.autorestart=true
unless-stopped Uses last-state behavior (Incus default)
services:
  app:
    image: docker.io/nginx:alpine
    restart: always

Restart enforcement is handled by the ic-healthd sidecar, including restart without a healthcheck - see Health Checking.