incus-compose lets you run your existing compose.yaml files directly on Incus without Docker.
oci_network_config API, which every
compose network attachment relies onpodman or docker for image building (see Builds)incus-compose requires the Incus server to listen on the network. Set
core.https_address:
incus config set core.https_address=:8443
This is not optional, even for a local Incus reached over the Unix socket.
incus-compose caches images in a separate Incus project and copies each one into
your project on up. That cross-project copy uses Incus pull mode, which needs
the server to be reachable over the network: the same daemon pulls the image
from itself. Without core.https_address set, up fails with
The source server isn't listening on the network, and health checks are
silently skipped.
Only the server setting matters here; the client connection itself can stay on the Unix socket. See Local vs Remote Incus for the handful of behaviours that do depend on how you connect.
Connect the client over HTTPS when Incus runs on another host, or when you use
health checks: the ic-healthd sidecar reaches Incus over HTTPS. By default
healthd uses the project's own network and reaches Incus over that bridge; use
--healthd-network / --healthd-incus if your setup differs, see
Network Configuration.
# Generate and trust a certificate
incus remote generate-certificate
incus config trust add-certificate ~/.config/incus/client.crt
incus remote add local-https <a-ip-of-your-host>
# Switch to local-https as default remote
incus remote switch local-https
incus list --all-projects
If you don't want to listen on all interfaces, set the
INCUS_COMPOSE_HEALTHD_INCUS environment variable or call up with
--healthd-incus; see Network Configuration.
The install script downloads the matching release for your OS/architecture and verifies it against the published SHA-256 checksums.
# Into a user-writable directory on your PATH (no sudo and self-update working)
curl -sSfL https://raw.githubusercontent.com/lxc/incus-compose/main/install.sh | sh -s -- -b ~/.local/bin
# Or System-wide into /usr/local/bin
curl -sSfL https://raw.githubusercontent.com/lxc/incus-compose/main/install.sh | sudo sh -s -- -b /usr/local/bin
Pass a release tag as the final argument to pin a version, e.g.
... | sudo sh -s -- -b /usr/local/bin 1.0.0-beta15. Without a tag the latest
release is installed.
Download a prebuilt archive from the Releases Page.
On Windows and MacOS, incus-compose runs as a client that drives a remote Incus host over HTTPS - see Installing on Windows.
# Build from source
git clone https://github.com/lxc/incus-compose
cd incus-compose
just build
# Or install directly
go install github.com/lxc/incus-compose/cmd/incus-compose@latest
services:
web:
image: docker.io/nginx:alpine
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
app:
image: docker.io/node:20-alpine
working_dir: /app
volumes:
- ./app:/app
command: node server.js
depends_on:
- web
incus-compose up
This will:
flowchart TD
S([incus-compose up]) --> P["create the Incus project,<br/>named after your directory"]
P --> HD{"any healthcheck, restart policy,<br/>or service_healthy dependency?"}
HD -->|yes| SC[start the ic-healthd sidecar]
HD -->|no| IMG
SC --> IMG{"image already in the cache?"}
IMG -->|yes| CP[copy it into the project]
IMG -->|no| PULL["pull from the registry,<br/>store it in the cache"]
PULL --> CP
CP --> NV[create networks and volumes]
NV --> INST["create and start instances<br/>in dependency order"]
INST --> WAIT["wait for service_healthy<br/>dependencies to report healthy"]
If your compose file uses health checks, incus-compose manages the ic-healthd sidecar automatically. It is transparent during normal use, but it is also a core component: all healthcheck, restart: and depends_on: service_healthy behavior is enforced by this sidecar, not by Incus. A working healthd is also required to bring up a project that has service_healthy dependencies - up waits for healthd to report them healthy, so a broken healthd makes up hang and fail (unless you pass --no-healthd). If health, restart, or startup behavior ever looks wrong, debug healthd first - see Health Checking and Debugging ic-healthd.
incus-compose list
# View logs from all services
incus-compose logs
# Follow logs in real-time
incus-compose logs -f
# View logs from specific services
incus-compose logs web app
# Stop and remove containers
incus-compose down
# Also remove images used by the services
incus-compose down --images
# Remove the whole project, including volumes and images (--volumes is an alias)
incus-compose down --project
compose.incus.yaml is loaded automatically when it exists next to the selected compose.yaml. This lets you keep an upstream or Docker-focused Compose file unchanged while adding Incus-specific settings in a separate file.
Typical uses:
ports: !reset []ic-healthdx-incusExample compose.incus.yaml:
services:
web:
ports: !reset []
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost"]
networks:
default:
ipv4_address: 10.131.32.17/24
networks:
default:
x-incus:
ipv4.nat: "true"
ipv4.address: 10.131.32.1/24
The file follows normal Compose merge rules. For example, !reset [] clears a list from the base file. See Compose Compatibility for details.
services:
db:
image: docker.io/postgres:16-alpine
environment:
POSTGRES_PASSWORD: dev123
volumes:
- pgdata:/var/lib/postgresql/data
api:
image: docker.io/myapp/api:latest
depends_on:
- db
environment:
DATABASE_URL: postgres://postgres:dev123@db/myapp
web:
image: docker.io/myapp/frontend:latest
depends_on:
- api
ports:
- "3000:80"
volumes:
pgdata:
Services start in dependency order:
flowchart LR
DB[db] -->|"api depends_on db"| API[api]
API -->|"web depends_on api"| WEB[web]
# .env
DB_PASSWORD=secret123
API_PORT=3000
services:
api:
image: docker.io/myapp/api:latest
environment:
DATABASE_PASSWORD: ${DB_PASSWORD}
ports:
- "${API_PORT}:3000"
Only variables defined in .env are available (not your shell environment).
Incus gives each container a real IP on your network:
$ incus-compose list
KIND NAME INCUSNAME IMAGE STATUS ADDRESSES
image docker.io/nginx:alpine docker.io/library/nginx:alpine Exists
network default ic-ynmt73wxwq Exists
instance web-1 web-1 docker.io/library/nginx:alpine Running 10.149.206.30
You can access containers directly: curl http://10.149.206.30
Published ports use Incus proxy devices (not iptables NAT):
ports:
- "8080:80" # Host 8080 → Container 80
Named volumes are Incus custom storage volumes with automatic UID/GID shifting:
volumes:
data:/app/data # Named volume with proper permissions
./local:/app # Bind mount (incusd must be on this machine)
A bind mount is passed through to incusd, which opens the path on its own
filesystem, so it works when the server is this machine, over the Unix socket
or over HTTPS. Against a server elsewhere, either use a named volume or set
x-incus-compose.seed: true
to copy the files across.
Each network becomes an Incus bridge network with deterministic naming:
networks:
frontend:
backend:
Long network names are hashed to fit Linux interface limits (13 chars for dhclient compatibility).
Each compose project gets its own Incus project:
$ incus-compose -p myapp up
# Creates Incus project "myapp"
$ incus-compose -p testing up
# Separate Incus project "testing"
Projects are isolated: separate networks, volumes, and instances.
Images are cached in either the incus-compose-cache project or the project you set via the INCUS_COMPOSE_IMAGE_CACHE env:
$ incus project list
+---------------------------+--------+----------+-----------------+-----------------+----------+---------------+------------------------------------------+---------+
| NAME | IMAGES | PROFILES | STORAGE VOLUMES | STORAGE BUCKETS | NETWORKS | NETWORK ZONES | DESCRIPTION | USED BY |
+---------------------------+--------+----------+-----------------+-----------------+----------+---------------+------------------------------------------+---------+
| default (current) | YES | YES | YES | YES | YES | YES | Default Incus project | 14 |
+---------------------------+--------+----------+-----------------+-----------------+----------+---------------+------------------------------------------+---------+
| immich | YES | YES | YES | YES | NO | NO | incus-compose: immich | 7 |
+---------------------------+--------+----------+-----------------+-----------------+----------+---------------+------------------------------------------+---------+
This means:
incus-compose down only removes project images, cache persistsFor a technical background about images see architecture/client/image.md
The cache project is created automatically on first use.