High-level architecture of incus-compose and how components fit together, a resource-first design:
incus-compose/
├── cmd/incus-compose/ # CLI entry point
├── client/ # Incus client with resources, stack, pool
└── project/ # Compose-spec to Incus translation
cmd/incus-compose/
client/
project/
flowchart LR
CMD[cmd/incus-compose]
PRJ[project]
CLI[client]
CMD -->|"creates GlobalClient, runs Stack"| CLI
CMD -->|"loads compose"| PRJ
PRJ -->|"calls client.Resource()"| CLI
The CLI creates a GlobalClient and loads the compose project. Then project takes over: it reads the compose definitions and configures resources on the client. The client owns the resources, but project drives what gets created.
This means project is not a passive loader. It actively builds the resource graph by calling into client. The Stack returned by project contains all resources ready for execution.
flowchart TD
GC[GlobalClient]
GC --> IC[("imageCache<br/>default project<br/>INCUS_COMPOSE_IMAGE_CACHE")]
GC --> C["Client<br/>project-scoped"]
C --> PR[Profile]
C --> IM[Image]
C --> NW[Network]
C --> SV[StorageVolume]
C --> IN[Instance]
IN --> DEV["Devices<br/>pre-creation"]
IN --> PDEV["PostDevices<br/>post-creation"]
Images go through three stages:
incus-compose-cache project (configurable via
INCUS_COMPOSE_IMAGE_CACHE)flowchart LR
R["registry<br/>docker.io, ghcr.io"] -->|"pull (slow)"| C[("cache project<br/>incus-compose-cache")]
C -->|copy| P[compose project]
P -->|use| I[instance]
Benefits:
down/up cyclesConfiguration phase - Resource created in memory
image, _ := client.Resource(KindImage, "docker.io/alpine", &ImageConfig{})
image.Config.Source = imageServer // configure
Execution phase - Resource created on Incus
image.Ensure(OptionCreate()) // blocks, creates on server
See Client Package for Stack, WorkerPool, resource ordering, and hook details.
My_Project! -> my-project
Valid DNS names, max 63 chars, long names hashed to 32 hex chars.
Linux interface limit (13 chars), uses hash for long names: backend ->
app-backend or ic-a1b2c3d4e5
See Errors for sentinel errors and context enrichment.
The CLI dials a remote of the Incus CLI configuration: --remote, else
INCUS_REMOTE, else the configured default.
incus remote add ci https://192.168.1.100:8443
incus-compose --remote ci up
client.New also takes a connection built elsewhere. DialRemote is the same
path the CLI takes; anything reachable through iclient works.
conn, err := client.DialRemote("", "ci")
gc := client.New(ctx, client.ClientProvideConnection(conn))
The connection is a *iclient.Connection, our fork of the Incus client. The
upstream one shares event-listener state between everything holding it, so a
single connection cannot be driven from several goroutines - which is exactly
what the WorkerPool does. One connection serves every
project: each call names the project it acts on, so EnsureProject hands back a
Client that carries a project name, not a connection of its own. See
iclient.
.env files can use OS variables for interpolation--os-env flag for Docker Compose compatibilityPass raw Incus configuration options directly to instances and networks:
services:
web:
image: docker.io/nginx:alpine
x-incus:
limits.memory: 512MiB
limits.cpu: "2"
security.nesting: "false"
networks:
custom:
x-incus:
nat: "false"
ipv4.nat: "true"
All key-value pairs are passed verbatim to Incus. See the
Incus instance options reference
for available options, and
Compose Compatibility for
the per-resource (instance, network, volume) x-incus reference.
Compose-specific transformations and conveniences handled by incus-compose:
x-incus-compose:
healthd:
incus: https://:8443
network: :default
services:
app:
image: docker.io/myapp:latest
healthd.incus and healthd.network configure where the ic-healthd sidecar
attaches and which Incus endpoint it connects to. Both default to the project's
own network and the connection's port; see
Health Checking - Network Configuration for
the full set of combinations.
incus-compose up # Start services
incus-compose up --no-start # Create without starting
incus-compose up --recreate # Recreate existing containers
incus-compose down # Stop and remove
incus-compose down --volumes # Also remove volumes
incus-compose list # List running containers
incus-compose config --quiet # Validate compose file
incus-compose config # Show resolved configuration
incus-compose config --services # List service names
incus-compose config --networks # List network names
incus-compose config --volumes # List volume names
incus-compose config --environment # Show interpolation environment
# Basic service
services:
web:
image: docker.io/nginx:alpine
ports:
- "8080:80"
# With dependencies
services:
db:
image: docker.io/postgres:16-alpine
app:
image: docker.io/myapp:latest
depends_on:
- db
# With named volume
services:
app:
image: docker.io/myapp:latest
volumes:
- data:/var/lib/app
- ./config:/etc/app:ro
volumes:
data:
# With environment file
services:
app:
image: docker.io/myapp:latest
environment:
DATABASE_URL: ${DATABASE_URL}
env_file:
- .env
See the docs index for all user and contributor docs. Closely related: