Incus Source¶
The Incus source creates DNS A records for running Incus system containers and virtual machines. It polls the Incus REST API to discover instances, resolves each instance's IP address from its network state, then registers an A record mapping <instance-name>.<domain> to that IP. It also subscribes to the Incus event stream so instance changes are reflected in DNS almost immediately (see Event-Driven Updates).
It connects either to a local Unix socket (agent running on the Incus host) or to a remote HTTPS endpoint secured with a client certificate.
How It Works¶
flowchart LR
A["Incus API<br/>/1.0/instances?recursion=2"] -->|"Poll interval"| B["List containers / VMs"]
B -->|"Filter: state"| C["Resolve IP"]
C -->|"InstanceState.Network<br/>(first global inet addr)"| D["Hostname + IP"]
D --> E["Incus Source"]
E -->|"A record"| F["Reconciler → DNS"]
- Lister polls
/1.0/instances?recursion=2and applies the state filter - IP resolver scans each instance's network interfaces, skipping loopback and non-routable addresses, and selects the first global IPv4 address
- Source maps the instance name + configured domain suffix to a fully-qualified hostname
- Reconciler creates or updates A records via the matching DNS provider
A background event watcher subscribes to the Incus event stream and triggers an out-of-band reconcile whenever an instance changes, so updates do not have to wait for the next poll. See Event-Driven Updates.
Event-Driven Updates¶
Alongside the periodic poll, the Incus source runs an event watcher that
subscribes to the Incus event stream at /1.0/events (filtered to lifecycle
events). When an instance is created, started, stopped, or deleted, the watcher
triggers a reconcile within a short debounce window, giving near-instant DNS
updates instead of waiting for DNSWEAVER_RECONCILE_INTERVAL.
flowchart LR
A["Incus event stream<br/>/1.0/events (lifecycle)"] -->|"instance-started<br/>instance-stopped<br/>..."| B["Event watcher"]
B -->|"debounce 2s"| C["Trigger reconcile"]
C --> D["Lister re-reads state<br/>/1.0/instances"]
D --> E["Reconciler → DNS"]
Design notes:
- The lister remains the source of truth. Events only trigger a reconcile;
the current instance state (and each instance's IP) is always re-read from
/1.0/instances. The periodic poll stays enabled as a safety net for any missed event. - Bursts coalesce. A 2-second debounce collapses a burst of events (for
example, an
incus-compose upthat starts many instances at once) into a single reconcile. - Resilient connection. The watcher reconnects with a short backoff if the stream drops, and shuts down cleanly on exit. It reuses the same transport as the lister, so it works over both the local Unix socket and a remote HTTPS + TLS endpoint.
- No extra configuration. The watcher starts automatically whenever the
Incus source is active (
DNSWEAVER_INCUS_URLorDNSWEAVER_INCUS_SOCKET_PATHis set).
Metrics¶
| Metric | Type | Description |
|---|---|---|
dnsweaver_incus_events_processed_total{action} |
counter | Incus events processed, labeled by lifecycle action (e.g. instance-started). |
dnsweaver_incus_watcher_reconnects_total |
counter | Number of times the event watcher reconnected after a stream error. |
Multiple Projects¶
Incus organizes instances into projects. By default the Incus source watches a single project. A single dnsweaver instance can watch many projects at once, so one deployment can serve an entire Incus cluster rather than running one dnsweaver per project.
There are three modes, in order of precedence:
| Mode | Configuration | Behavior |
|---|---|---|
| All projects | DNSWEAVER_INCUS_ALL_PROJECTS=true |
Watch every project the credentials can see, including projects created later. Lowest friction for large deployments. |
| Explicit list | DNSWEAVER_INCUS_PROJECTS=team-a,team-b |
Watch exactly the listed projects. Projects that do not exist yet are still watched and picked up the moment they appear. |
| Single project | DNSWEAVER_INCUS_PROJECT=team-a |
Watch one project (the original behavior). Empty = the Incus default project. |
DNSWEAVER_INCUS_PROJECTS also accepts * or all as a shorthand that is
equivalent to DNSWEAVER_INCUS_ALL_PROJECTS=true.
Each instance's project is preserved in its workload metadata, so records stay correctly attributed no matter how many projects are in scope. Both the periodic lister and the event watcher honor the selected scope: all-projects mode uses one stream for the whole server, while an explicit list uses one stream per project.
All-projects requires unrestricted credentials
DNSWEAVER_INCUS_ALL_PROJECTS=true (and the * / all shorthand) use the
Incus API's all-projects mode, which requires server-wide (unrestricted)
credentials. A project-restricted certificate cannot list or watch other
projects — use DNSWEAVER_INCUS_PROJECTS with an explicit list in that case.
# Watch an entire Incus cluster with one dnsweaver instance
DNSWEAVER_INCUS_URL=https://incus-host:8443
DNSWEAVER_INCUS_ALL_PROJECTS=true
DNSWEAVER_INCUS_DOMAIN_SUFFIX=home.example.com
Configuration¶
Environment Variables¶
| Variable | Required | Default | Description |
|---|---|---|---|
DNSWEAVER_INCUS_URL |
Alt | — | Remote Incus API base URL, e.g. https://incus-host:8443. Mutually exclusive with DNSWEAVER_INCUS_SOCKET_PATH. |
DNSWEAVER_INCUS_SOCKET_PATH |
Alt | — | Path to the local Incus Unix socket, e.g. /var/lib/incus/unix.socket. Mutually exclusive with DNSWEAVER_INCUS_URL. |
DNSWEAVER_INCUS_PROJECT |
No | (all / default) | Restrict discovery to a single Incus project |
DNSWEAVER_INCUS_PROJECTS |
No | — | Comma-separated list of Incus projects to watch, e.g. team-a,team-b. Use * or all as a shorthand for all projects. See Multiple Projects. |
DNSWEAVER_INCUS_ALL_PROJECTS |
No | false |
Watch every Incus project via the API's all-projects mode. Requires server-wide (unrestricted) credentials. See Multiple Projects. |
DNSWEAVER_INCUS_STATE_FILTER |
No | running |
Instance status filter (running, stopped, etc.) |
DNSWEAVER_INCUS_DOMAIN_SUFFIX |
No | — | Domain suffix appended to instance names, e.g. home.example.com |
DNSWEAVER_INCUS_TARGET_MODE |
No | guest-ip |
Target resolution mode. guest-ip (default) emits an A record per instance IP. instance defers record type and target to the matching provider instance — useful for pointing all instances at a reverse proxy via CNAME. |
DNSWEAVER_INCUS_TLS_CA_FILE |
No | — | Path to PEM CA bundle that issued the Incus server certificate (remote HTTPS). |
DNSWEAVER_INCUS_TLS_CERT_FILE |
No | — | Client certificate for mutual TLS against the Incus API (pair with TLS_KEY_FILE). |
DNSWEAVER_INCUS_TLS_KEY_FILE |
No | — | Client private key for mutual TLS. |
DNSWEAVER_INCUS_TRUST_TOKEN |
No | — | One-time Incus trust token to enroll a client certificate. See Trust-Token Authentication. |
DNSWEAVER_INCUS_CERT_STORE |
No | — | Writable directory where the enrolled client keypair is persisted. Required with TRUST_TOKEN. |
DNSWEAVER_INCUS_TLS_SERVER_NAME |
No | — | SNI / verification hostname override. |
DNSWEAVER_INCUS_TLS_MIN_VERSION |
No | 1.2 |
Minimum TLS protocol version (1.2 or 1.3). |
DNSWEAVER_INCUS_TLS_SKIP_VERIFY |
No | false |
Skip Incus TLS certificate verification. Prefer TLS_CA_FILE or TLS_PIN_SHA256. Logs a warning when enabled. |
DNSWEAVER_INCUS_TLS_PIN_SHA256 |
No | — | Pin the Incus server's leaf certificate to this SHA-256 fingerprint (hex, colons optional). Verifies a self-signed server without a CA file. See Server verification. |
Pick exactly one endpoint
Set either DNSWEAVER_INCUS_URL (remote HTTPS) or
DNSWEAVER_INCUS_SOCKET_PATH (local socket) — never both. Setting both is a
configuration error and dnsweaver will refuse to start.
Source Registration¶
Add incus to DNSWEAVER_SOURCES:
Auto-registration
When DNSWEAVER_INCUS_URL or DNSWEAVER_INCUS_SOCKET_PATH is set, the Incus
source is automatically registered even if not listed in
DNSWEAVER_SOURCES. You only need to list it explicitly if you want to
control source ordering relative to other sources.
Hostname Resolution¶
The source determines the DNS hostname for each instance using this logic, in order of precedence:
user.dnsweaver.hostnameconfig key — set on the instance, its value is used verbatim as the hostname (e.g.incus config set web user.dnsweaver.hostname=app.example.net)dnsweaver.hostnamelabel — the incus-compose form of the override; used verbatim when the nativeuser.dnsweaver.hostnamekey is not set- Instance name contains a dot — used directly as an FQDN (e.g.,
db.home.example.com) - Domain suffix configured — appended to the instance name (e.g.,
web+home.example.com→web.home.example.com) - None of the above — the instance is skipped; a debug log entry is emitted
Domain suffix is strongly recommended
Without a domain suffix, only instances whose names already contain a dot (or
that set user.dnsweaver.hostname) will produce DNS records. Set
DNSWEAVER_INCUS_DOMAIN_SUFFIX to ensure all instances are registered.
incus-compose Labels¶
incus-compose runs Docker Compose files
against Incus. Since v1.0.0-rc.2 it stores each Compose labels: entry as an
instance config key prefixed user.label. — for example, a Compose label
traefik.http.routers.app.rule becomes the instance config key
user.label.traefik.http.routers.app.rule.
dnsweaver's Incus adapter surfaces every instance config key as a workload
label, and additionally exposes each user.label.<key> under its stripped
<key> form. The raw user.label.* key is always retained for transparency,
and a stripped alias never overwrites a label that is already present.
This means the existing label-based sources consume incus-compose labels with
no extra configuration — just enable the source that matches your labels
alongside incus:
| Compose label | Stored as | Read by source |
|---|---|---|
dnsweaver.hostname=app.example.com |
user.label.dnsweaver.hostname |
dnsweaver (or the Incus source's own override) |
traefik.http.routers.app.rule=Host(...) |
user.label.traefik.http.routers.app.rule |
traefik |
caddy=app.example.com |
user.label.caddy |
caddy |
Example — a Compose service whose Traefik router labels drive DNS:
services:
app:
image: docker.io/nginx:alpine
labels:
traefik.enable: "true"
traefik.http.routers.app.rule: "Host(`app.example.com`)"
Two always-present labels
incus-compose always adds user.label.incus-compose.project and
user.label.incus-compose.service (the Compose project and service names).
These are surfaced de-prefixed as incus-compose.project /
incus-compose.service and can be read by any source or used for debugging.
Per-instance hostname override¶
Pin an arbitrary FQDN to any instance with a config key:
This takes precedence over the derived <name>.<domain> hostname.
IP Address Resolution¶
The resolver reads each instance's live network state (InstanceState.Network) and selects an address by:
- Sorting interface names for deterministic selection
- Skipping loopback interfaces (
lo,lo0) - Selecting the first global IPv4 (
inet) address - Skipping non-routable addresses (link-local, etc.)
Tailscale / CGNAT addresses are kept
Addresses in the 100.64.0.0/10 CGNAT range (used by Tailscale) are treated
as valid targets, so Tailscale-connected instances resolve to their tailnet IP.
Instances with no resolvable IP are skipped in both target modes — the IP existence acts as a liveness gate.
Target Mode¶
DNSWEAVER_INCUS_TARGET_MODE controls what the source emits for each discovered instance:
| Mode | Record Type | Target | Use Case |
|---|---|---|---|
guest-ip (default) |
A |
Instance's resolved IP | Direct DNS resolution to each container/VM |
instance |
from instance | from instance | Point all Incus-discovered hostnames at a reverse proxy |
In instance mode, the source emits the hostname only (no record-type or target hints).
The matching provider instance's RECORD_TYPE and TARGET drive the resulting record,
so a CNAME instance pointed at NPMplus / Traefik / Caddy will create CNAME records for
every Incus instance that matches its DOMAINS filter.
Example: CNAME everything to a reverse proxy¶
DNSWEAVER_SOURCES=incus
DNSWEAVER_INCUS_SOCKET_PATH=/var/lib/incus/unix.socket
DNSWEAVER_INCUS_DOMAIN_SUFFIX=home.example.com
DNSWEAVER_INCUS_TARGET_MODE=instance # opt in
DNSWEAVER_INSTANCES=npmplus
DNSWEAVER_NPMPLUS_TYPE=technitium
DNSWEAVER_NPMPLUS_RECORD_TYPE=CNAME
DNSWEAVER_NPMPLUS_TARGET=npmplus.home.example.com # all instances point here
DNSWEAVER_NPMPLUS_DOMAINS=*.home.example.com
DNSWEAVER_NPMPLUS_URL=https://technitium.home.example.com
DNSWEAVER_NPMPLUS_TOKEN_FILE=/run/secrets/technitium_token
Every Incus instance matching *.home.example.com will get a CNAME pointing to
npmplus.home.example.com instead of an A record pointing at the instance's own IP.
Remote HTTPS Access¶
To reach Incus over the network, add a trust certificate to the Incus server and point dnsweaver at the API with a matching client certificate:
# On the Incus host: expose the API and trust dnsweaver's client cert
incus config set core.https_address :8443
incus config trust add-certificate dnsweaver.crt
Then configure dnsweaver:
DNSWEAVER_INCUS_URL=https://incus-host.home.example.com:8443
DNSWEAVER_INCUS_TLS_CERT_FILE=/run/secrets/incus_client_cert
DNSWEAVER_INCUS_TLS_KEY_FILE=/run/secrets/incus_client_key
DNSWEAVER_INCUS_TLS_CA_FILE=/run/secrets/incus_server_ca
Local socket needs no TLS
When using DNSWEAVER_INCUS_SOCKET_PATH, no TLS configuration is required —
access is governed by Unix socket file permissions. Mount the socket into the
container and ensure the dnsweaver process can read it.
Server verification¶
Incus's default server certificate only carries loopback Subject Alternative
Names (127.0.0.1, ::1). Verifying it by hostname against a LAN address
therefore fails with x509: certificate is valid for 127.0.0.1, ::1, not <ip>.
There are three ways to make a remote HTTPS connection verify:
- CA file (
DNSWEAVER_INCUS_TLS_CA_FILE): supply the PEM that issued the server certificate. Standard chain verification applies. Best when you run your own CA. - Fingerprint pin (
DNSWEAVER_INCUS_TLS_PIN_SHA256): pin the server's leaf certificate SHA-256. No CA file needed, still cryptographically anchored. Get the fingerprint from the Incus host withincus config trust listor `openssl s_client -connect host:8443