Skip to main content

cluster.yaml

cluster.yaml describes the whole cluster in one file: its networks, drives, nodes, dashboard, failover timers, sign-in, reverse proxy, Cloudflare DNS, self-update, git backup and AI access. Every node holds a copy at /etc/clouduler/cluster.yaml. A daemon refuses to start if the file does not parse or validate.

This page lists every key. Most keys apply live, without a restart. The Reload column in each table tells you which ones do not.

The file on disk is not the source of truth

Once a cluster exists, the authoritative copy of the config lives in the cluster's replicated state (raft). Every node rewrites its local cluster.yaml from that copy. If you edit the file by hand, the next sync reverts it, and a daemon restart does not help: your edit runs for a few seconds, then it is overwritten.

To apply a file edit, run clouduler config reload. Or skip the file and use the Settings pages in the web UI.

How the config is replicated​

  1. The config is stored in raft as one YAML string plus a generation number. The first stored config is generation 1.
  2. The web UI (Apply), clouduler config reload, clouduler node remove, the self-update settings and the network and drive rename buttons all write to raft.
  3. Every node watches raft. When the generation changes it parses the new YAML, hands it to the subsystems (proxy, DNS, drives, scheduler, auth) and rewrites its own file if the text differs. The check runs every failover.probe_interval (5 seconds by default).
  4. At boot a daemon reads its local file first, then the first watcher tick overwrites the file with the raft copy if they differ.
  5. A fresh cluster seeds raft from the founding node's file, but only while raft has no config at all.

See Live config for what takes effect immediately and what needs a restart.

Editing the config​

Open Settings. The tabs that edit cluster.yaml are General, Network, Storage and Sign-in and backup. Updates and API keys and AI clients are separate screens.

/settings/general
Settings, General: cluster name and failover timers.
  1. Change fields on any page. Changes are staged in a draft. The header button Apply changes stays disabled until the draft differs from the running config.
  2. Open Changes to apply to see the diff. Validate config checks the draft without applying it, Discard edits throws it away.
  3. Set Confirm within (seconds) (default 60, minimum 5).
  4. Press Apply changes. The UI validates first, then submits.
  5. If your edit touched networks, nodes, web, proxy or cloudflare, a rollback timer starts. The banner Config change waiting for you counts down and the header button turns into Confirm config. Press it once you can still reach the dashboard. Roll back now restores the previous config immediately.

Edits to failover, secrets, auth, drives, self_update, backup, mcp and cluster.name apply at once with no countdown.

Only admins can apply. Viewers can read.

/settings/network
Settings, Network: networks, dashboard, proxy and Cloudflare.
PageWhereKeys
Cluster/settings/generalcluster.name
Failover/settings/general?page=failoverfailover.*
Networks/settings/network?page=networksnetworks.* and the rename button
Dashboard/settings/network?page=webweb.*
Reverse proxy/settings/network?page=proxyproxy.enabled, ports, proxy.acme.*, proxy.dns_server.*
Cloudflare DNS/settings/network?page=cloudflarecloudflare.zones and the API token
Drives/settings/storage?page=drivesdrives.*
Sign-in/settings/security?page=authauth.*
Service login/settings/security?page=service-loginproxy.auth.*
Cluster key/settings/security?page=secretssecrets.generate_if_missing
AI access/settings/security?page=mcpmcp.enabled
Git backup/settings/security?page=git-backupbackup.*
Updates/settings/self-updateself_update.*
A node's page/nodes/<name>nodes.<name>.* (its Save button has its own Rollback after (seconds) field)

Annotated example​

A three-node cluster with an internal network, a home LAN, a public network and Cloudflare. The comments below are lost the first time you save through the UI or config reload, so treat this as a reading aid.

cluster.yaml
cluster:
name: homelab # shown in the UI and in alerts

networks:
internal: # exactly one internal network, used for raft, drives, overlay
type: internal
subnet: 10.10.0.0/24
domain: int.example.com # names under it resolve to node addresses
lan: # an extra subnetted LAN
type: private
subnet: 192.168.1.0/24
domain: lan.example.com
publish_dns: false # keep these names out of Cloudflare
wan: # reachable from the internet, no subnet
type: public
domain: example.com
vpn: # outbound-only, kill-switched; no domain allowed
type: egress

drives:
fast:
mode: replicated # every implementing node keeps a copy
sync_interval: 5m
bulk:
mode: served # one node serves it, others mount it over NFS
served_by: [atlas]

nodes:
atlas:
id: 00000000-0000-4000-8000-000000000001 # minted by `clouduler init`
role: manager
network:
internal: 10.10.0.1
lan: 192.168.1.10
wan:
ipv4: 203.0.113.5 # publishes an A record
open_ports: # ports the router forwards to this node
- {port: 25, protocol: tcp}
vpn: wg0 # host interface the egress network leaves through
drives:
fast: /srv/clouduler/fast
bulk: /srv/clouduler/bulk
labels:
disk: ssd
boreas:
id: 00000000-0000-4000-8000-000000000002
role: manager
network:
internal: 10.10.0.2
lan: 192.168.1.11
drives:
fast: /srv/clouduler/fast
cirrus:
id: 00000000-0000-4000-8000-000000000003
role: manager
last_resort: true # only schedule here when nothing else fits
network:
internal: 10.10.0.3
lan: 192.168.1.12
drives:
fast: /srv/clouduler/fast

data_dir: /var/lib/clouduler

web:
address: cloud # becomes cloud.int.example.com
network: internal
port: 8443
through_proxy: true
backend_port: 8444

failover:
node_timeout: 60s
fence_timeout: 15s
probe_interval: 5s
stop_grace: 30s # 15s + 30s < 60s, so no margin warning

secrets:
key_file: /etc/clouduler/secrets.key
generate_if_missing: true
node_key_file: /etc/clouduler/node.key

auth:
api_keys: true
session_ttl: 12h
oidc:
issuer: https://id.example.com/application/o/clouduler/
client_id: clouduler
scopes: [openid, profile, email, groups]
admin_claim: groups
admin_value: clouduler-admins
username_claim: preferred_username

cloudflare:
zones:
- example.com # every network domain must sit inside a zone

proxy:
enabled: true
http_port: 80
https_port: 443
dns_publish: all-nodes # or owning-node
dns_server:
enabled: false
acme:
enabled: true
directory_url: https://acme-staging-v02.api.letsencrypt.org/directory # staging while testing
contact_email: [email protected]
auth:
host: login
networks: [lan]
issuer: https://id.example.com/application/o/apps/
client_id: clouduler-apps
palette: grape

backup:
repo_url: [email protected]:homelab/backup.git
age_recipient: age1examplepublickeyexamplepublickeyexamplepublickeyexample00
branch: main

mcp:
enabled: true

self_update is not shown: it is written through the Updates page. Only cluster, networks, nodes and web are required. Everything else has a default.

Options​

Reload is live when the value takes effect without restarting the daemon, and restart when it is read once at boot. See Live config.

cluster​

Option
Type / default
Reload
Description
cluster.namerequired
stringno default
live reload
Cluster name shown in the UI and alert text. Trimmed, must not be empty.
Watch out: Raft keeps the name it saw at boot; the UI and alerts read the new name live.

networks​

Network names are referenced by nodes, web.network, proxy.auth.networks and the networks: lists of stacks. Use the rename button in Settings > Network instead of renaming a key by hand: it also rewrites deployed stacks and the redirect, static-route and WireGuard stores. See Networking.

Option
Type / default
Reload
Description
networksrequired
mapno default
live reload
Every network a node, stack or DNS record can use. Needs at least one, and exactly one of type internal.
Watch out: Errors: exactly one internal network is required for node to node traffic; only one internal network is allowed, found NAMES.
enumno default
live reload
internal, private, public, egress, wireguard or nat. Internal is the node-to-node network (raft, drive sync, overlay). Private is an extra LAN with a subnet. Public has no subnet and resolves to the node's public address. Egress, wireguard and nat are outbound-only.
Watch out: Unknown value: unknown variant, expected one of internal, private, public, egress, wireguard, nat.
CIDRno default
live reload
Address range nodes get their IP from. Required for internal and private, forbidden for the other kinds.
Watch out: The internal subnet and all node IPs can be changed in one edit, no restart (see [Changing a node's address](/docs/features/node-lifecycle#changing-a-nodes-address) and the [multi-site quick start](/docs/quickstart/multi-site-headscale)). Error text reads a internal network needs a subnet (the article is wrong for every kind).
bare domainno default
live reload
Names under this domain resolve to node addresses on this network. Unique across networks. Not allowed on egress, wireguard or nat. Must sit under a cloudflare zone when cloudflare is set.
Watch out: Errors: must be a bare domain name; DOMAIN is already claimed by another network; DOMAIN is not covered by any configured cloudflare zone.
booldefault true
live reload
When false, the network's names are not written to Cloudflare. Only proxy.dns_server answers them.
Watch out: false is allowed only on internal and private networks. A public network is always published.

drives​

Option
Type / default
Reload
Description
mapdefault {}
live reload
Cluster-wide directory namespaces. Volumes live inside a drive.
durationdefault 15m
live reload
How often each copy syncs (replicated) or the active server pushes to standbys (served).
Watch out: 0s passes validation. What the sync loop does with it is not verified.
enumdefault replicated
live reload
replicated: every node that implements the drive keeps a copy. served: one active copy on one node, mounted elsewhere over the internal network.
list of node namesdefault []
live reload
Candidate order for the active server of a served drive. The first reachable node serves.
Watch out: Only valid with mode: served. Each name must exist, be listed once and map the drive.

nodes​

Option
Type / default
Reload
Description
nodesrequired
mapno default
live reload
The machines. Needs at least one, and at least one with role manager.
Watch out: With 2 managers the quorum is 2, so losing either loses quorum.
UUIDno default
live reload
Identity for raft membership and the removed-node tombstone. Minted by clouduler init.
Watch out: Missing: nodes.NAME: missing field id.
enumno default
live reload
manager votes in raft and can lead. worker only runs stacks.
Watch out: Unknown value: expected manager or worker.
mapdefault {}
live reload
The node's address on each network it sits on. The internal network is mandatory.
Watch out: Error: every node must have an address on the internal network NAME.
IP addressno default
live reload
Bare IP inside the network's subnet. On the internal network it is also the node's RPC address.
Watch out: Errors: IP is outside the NET subnet SUBNET; IP is used by another node. Changing it on a running node, managers included, is supported: see [Changing a node's address](/docs/features/node-lifecycle#changing-a-nodes-address).
mapno default
live reload
Block with ipv4, ipv6, domain and open_ports. ipv4 publishes an A record, ipv6 an AAAA, domain a CNAME.
Watch out: A bare IP is an error: a public network takes an ipv4, ipv6 and/or domain block. An empty block: set at least one of ipv4, ipv6 or domain.
list of {port, protocol}default []
live reload
Ports the router forwards to this node. A raw x-clouduler.expose port on this network is placed only on a node that lists it. protocol is tcp (default) or udp.
interface name or mapno default
live reload
Bare host interface name, or a map with interface, open_ports and fallback ({interface, health_check_ip}). Letters, digits, _ . - and at most 15 bytes.
Watch out: fallback must name a different interface than the primary. Wireguard blocks are created by the config import in the UI.
noneno default
n/a
Must not be set. Every node implements a nat network the same way.
map drive to absolute pathdefault {}
live reload
Drives this node implements, with the local directory that backs each.
Watch out: Errors: no drive named DRIVE is declared under drives; must be an absolute path.
map string to stringdefault {}
live reload
Labels matched by deploy.placement.constraints as node.labels.KEY.
Watch out: No UI editor; tags are shown read-only. A change takes up to one probe interval to reach the scheduler.
booldefault false
live reload
Place work here only when no other node fits.
booldefault true
live reload
Whether this node times a 4 KiB write plus fsync in its data dir when asked for a connectivity report. The result is the node's disk_ms and drives the "Slow disk" cell in the Nodes page matrix.
Watch out: False means the node reports no disk figure and is never flagged. The probe writes and removes a .disk-probe file in the data dir and gives up after 8 s.
portdefault 7373
needs restart
Port other nodes dial for raft. The drive transport uses rpc_port+1 and the container relay rpc_port+2, so a node occupies three consecutive ports.
Watch out: Peers learn a new port live but the node keeps listening on the old one until restart.
absolute pathdefault cluster data_dir
needs restart
Overrides the cluster data dir for this node.
Watch out: Clouduler does not move existing data for you.
pathdefault docker default
n/a
Records dockerd's data-root for this node. Only clouduler init writes /etc/docker/daemon.json.
Watch out: Changing it later does not change docker. The UI help text suggests a docker restart is enough; it is not.
booldefault inherits secrets.generate_if_missing
needs restart
Whether this node may create the cluster key when it is missing. clouduler init --join sets false.
Watch out: Creating a second key makes secrets sealed by the first unreadable.

data_dir and web​

Option
Type / default
Reload
Description
absolute pathdefault /var/lib/clouduler
needs restart
Default data dir for every node. Holds raft state, drive copies and the database.
webrequired
objectno default
n/a
The admin dashboard.
web.addressrequired
stringno default
needs restart
Hostname the dashboard is served on, always over HTTPS. A bare label gets the domain of web.network appended; a trailing dot is used verbatim; @ names the apex.
Watch out: DNS and the proxy route follow a change live, but the direct listener's certificate is built at boot, so restart for that listener.
web.networkrequired
network nameno default
live reload
Network whose node address the dashboard record points at and whose domain qualifies a bare address.
Watch out: Error: no network named NAME.
portdefault 8443
needs restart
Direct HTTPS listener. Stays up even when the dashboard is also proxied.
Watch out: Must differ from web.backend_port and the proxy ports.
booldefault true
live reload
Also publish the dashboard through the reverse proxy on the standard HTTPS port.
portdefault 8444
needs restart
Loopback-only plain HTTP port the proxy forwards dashboard traffic to.
Watch out: If changed, the proxy route follows live but nothing listens on the new port until restart.

failover​

Option
Type / default
Reload
Description
objectdefault all defaults
n/a
Failure handling. See Failover for how the timers interact.
durationdefault 60s
needs restart
How long a node may be unreachable before the leader reschedules its work.
Watch out: The UI shows no restart notice.
durationdefault 15s
needs restart
How long a node may lack raft quorum before it stops its own containers.
Watch out: Must be shorter than node_timeout and longer than probe_interval.
durationdefault 5s
needs restart
How often the leader probes nodes. Also the cadence of the config watcher and the roster pass.
Watch out: Must be shorter than fence_timeout and not zero.
durationdefault 30s
live reload
How long a container gets to stop cleanly before it is killed, unless the service sets stop_grace_period. Maximum 600s, not zero.
Watch out: fence_timeout + stop_grace must stay below node_timeout, otherwise the daemon logs a warning. The defaults trigger it.

secrets​

Option
Type / default
Reload
Description
absolute pathdefault /etc/clouduler/secrets.key
needs restart
Cluster key used to decrypt stored secrets. Not editable in the UI, on purpose.
Watch out: Mode must be owner read/write with at most group read.
booldefault true
needs restart
Cluster-wide default for whether a node may create the key when the file is missing.
Watch out: Per-node override is nodes.<name>.generate_secret_key.
absolute pathdefault /etc/clouduler/node.key
needs restart
This node's own keypair. Its public half lets another key holder wrap the cluster key for it. Not editable in the UI.

auth​

The OIDC client secret is not in the file. Set it under Settings > Sign-in and backup > Sign-in. See Access and login.

Option
Type / default
Reload
Description
objectdefault {api_keys: true, session_ttl: 12h}
n/a
Sign-in to the dashboard and API.
booldefault true
live reload
Intended switch for API key sign-in.
Watch out: false is rejected unless auth.oidc is set. The research found that the flag is not enforced at runtime today: keys keep working even when false.
durationdefault 12h
live reload
How long a dashboard session lasts from login. No sliding renewal.
Watch out: Existing sessions keep their original expiry.
objectdefault absent
live reload
OIDC identity provider for people who operate the cluster. Absent means OIDC is off. The client secret is a sealed secret, not in this file.
https URLno default
live reload
Base URL. Discovery is read from ISSUER/.well-known/openid-configuration.
Watch out: Required when oidc is set. Error: must be an https url. Trailing slashes matter.
stringno default
live reload
OIDC client ID.
Watch out: Required when oidc is set.
listdefault [openid, profile, email]
live reload
Scopes requested at login. Must include openid.
Watch out: No UI control. Add groups here if your provider releases groups only by scope.
stringdefault groups
live reload
ID token claim (string or array) checked for admin_value.
stringdefault none
live reload
Value in the admin claim that grants admin. Blank means nobody becomes admin through OIDC; everyone else is a viewer.
Watch out: Matching is exact and case-sensitive.
stringdefault preferred_username
live reload
Claim shown as the signed-in user's name. Falls back to sub.

cloudflare​

The API token is a sealed secret set from Settings > Network > Cloudflare DNS. See Proxy and TLS and DNS publishing.

Option
Type / default
Reload
Description
objectdefault absent
live reload
Enables Cloudflare DNS management. The API token is a sealed cluster secret set from the UI.
Watch out: When present, zones must be non-empty.
list of zone namesno default
live reload
Zones Clouduler may write records into. The longest matching zone wins.
Watch out: Required when cloudflare is set. Every network domain and the dashboard hostname must sit inside one.

proxy​

Option
Type / default
Reload
Description
booldefault true
live reload
Runs the reverse proxy on every node.
portdefault 80
live reload
Plain HTTP listener. Serves tls: false routes, ACME HTTP-01 challenges and redirects everything else to HTTPS. Below 1024 needs root.
Watch out: Must differ from https_port, web.port and web.backend_port.
portdefault 443
live reload
TLS listener for routes.
Watch out: Error if equal to the dashboard port: proxy.https_port: web.port already listens on PORT.
enumdefault all-nodes
live reload
all-nodes: every node's address is published for every proxied hostname. owning-node: only the nodes running the containers are published, stale records are deleted and the scheduler co-locates containers that share a hostname.
Watch out: No UI control; edit the file. See DNS publishing.
booldefault false
live reload
Turns on the cluster DNS server that answers by the client's network.
Watch out: Re-read every 5 seconds.
list of network namesdefault [] (every subnetted network the node has an address on)
live reload
Networks to listen on, at the node's address on each. A public network is served only when listed.
Watch out: Errors: no network named NAME; NAME has no node address to listen on.
portdefault 53
live reload
UDP and TCP port of the cluster DNS server. Below 1024 needs root.
Watch out: Must not be 0.
booldefault false
live reload
Issue and renew certificates through ACME. Issuance is DNS-01 through Cloudflare.
Watch out: Needs a cloudflare section: proxy.acme.enabled: certificate issuance is DNS-01 only, so it needs a cloudflare section.
URLdefault Let's Encrypt production
live reload
ACME directory. Point at staging while testing.
Watch out: Not validated; a malformed URL is found only when a certificate is requested.
stringdefault none
live reload
Address registered with the CA for expiry warnings.
Watch out: Not validated.

proxy.auth​

Option
Type / default
Reload
Description
objectdefault absent
live reload
Service login: a portal that stands in front of apps whose routes use the authenticated middleware. A separate OIDC client from auth.oidc.
label or FQDNno default
live reload
Portal hostname. A bare label gets each listed network's domain appended.
Watch out: Required when proxy.auth is set. Must differ from the dashboard hostname.
list of network namesno default
live reload
Networks that offer the login. Each needs a domain. A route can require login only on a listed network.
Watch out: Required, at least one.
https URLno default
live reload
Identity provider for the portal.
Watch out: Required.
stringno default
live reload
Client ID for the portal. Its secret is sealed separately.
Watch out: Required.
listdefault [openid, profile, email, groups]
live reload
OIDC scopes. Must include openid.
Watch out: The UI takes space- or comma-separated text.
stringdefault groups
live reload
Claim listing the visitor's groups. Routes restrict by group name.
stringdefault preferred_username
live reload
Claim shown as the user's name on the portal. Falls back to sub.
Watch out: It is not passed to apps.
durationdefault 12h
live reload
Lifetime of a portal login and of each app cookie it issues. Must be greater than zero.
stringdefault Sign in to continue
live reload
Heading of the login page.
enumdefault grape
live reload
Colour set of the login page: grape, sunset, ocean or forest.
hex colourdefault none
live reload
Overrides the palette's main colour. The server accepts #rgb or #rrggbb.
Watch out: The UI accepts only six digits; a three-digit accent set in the file shows as unset in the UI.
list of network namesdefault []
live reload
Networks whose login page offers remember this IP address. Each must be listed in proxy.auth.networks and be internal, private or public.
durationdefault 30d
live reload
How long a remembered IP skips login, counted from the login that saved it. Must be greater than zero.

self_update​

Option
Type / default
Reload
Description
objectdefault absent
live reload
Git-based cluster self-update. Written through the Updates page, not the config editor.
Watch out: Once release_url is set it cannot be changed through the API; edit the file and run config reload.
URLno default
live reload
A repository URL (tracks the latest release) or a release tag URL (pins it). For the official releases use https://github.com/LiteApplication/clouduler.
Watch out: Required when self_update is set.
booldefault true
live reload
Whether the leader looks up the latest release on an interval.
durationdefault 24h
live reload
How often the lookup runs.
Watch out: The API enforces at least 1h; the file does not.

backup​

Option
Type / default
Reload
Description
objectdefault absent (off)
live reload
Mirrors cluster.yaml, stacks, configs and re-encrypted secrets into an admin-owned git repo.
SSH URLno default
live reload
Repository to push to. Clouduler generates its own deploy key for it.
Watch out: Required when backup is set. Only ssh URLs: only ssh urls are supported (e.g. git@host:org/repo.git), not http(s).
age1... public keyno default
live reload
Public key that secrets are re-encrypted to for the export. The private key stays outside Clouduler.
Watch out: Required. Error: must be an age X25519 recipient, starting with age1.
stringdefault main
live reload
Branch to push.
Watch out: Must not be empty.
legacydefault n/a
n/a
Removed field kept so older files still load. It is accepted and discarded.
Watch out: A UI save drops it.

mcp​

Option
Type / default
Reload
Description
booldefault true
live reload
Turns the MCP endpoint and its OAuth pages on or off. Off makes them answer 404.
Watch out: clouduler init writes false into a new cluster.

Validation errors you may see​

Validation stops at the first failure, so you see exactly one error, printed as Error: invalid config at <path>: <message>.

ErrorMeaning
cluster.name: must not be emptyGive the cluster a name.
networks.internal: a internal network needs a subnetSubnetted kinds need subnet. The article is wrong in the message for every kind.
networks.vpn.domain: a egress network is not addressable and must not declare a domainRemove domain from egress, wireguard and nat networks.
networks.wan: a public network is always published to dns; publish_dns can only be turned off on internal and private networksUse a private network for names you want to hide.
nodes.n1.network.internal: 10.20.0.1 is outside the internal subnet 10.10.0.0/24Node IP not inside the network's subnet.
nodes.n1.network.wan: a public network takes an ipv4, ipv6 and/or domain blockA public network needs a block, not a bare IP.
web.backend_port: must differ from web.portPick two different ports.
proxy.https_port: web.port already listens on 8443Proxy and dashboard ports clash.
web.network: no network named nopeReference to a network that does not exist.
failover.fence_timeout: must be shorter than node_timeout (10s), or a container can run on two nodes at onceFix the timers.
failover.probe_interval: must be shorter than fence_timeout (15s), or a node cannot be missed before it fencesFix the timers.
failover.stop_grace: must be at most 600sAlso rejected: zero.
auth.api_keys: cannot be disabled without an oidc provider, that would lock everyone outConfigure OIDC first.
drives.fast.served_by: only a served drive has a serving node, set mode: servedAdd mode: served.
networks.internal.domain: int.example.com is not covered by any configured cloudflare zoneAdd the parent zone to cloudflare.zones.
proxy.acme.enabled: certificate issuance is DNS-01 only, so it needs a cloudflare sectionAdd cloudflare.
backup.repo_url: only ssh urls are supported (e.g. git@host:org/repo.git), not http(s)Use an SSH URL.
backup.age_recipient: must be an age X25519 recipient, starting with age1Paste an age1... public key.
data_dir: must be an absolute pathSame for secrets.key_file, nodes.<name>.drives.<drive> and friends.

An edit that would make two deployed stacks share an overlay network is refused with this config would make stacks A and B share the overlay network OVERLAY: rename one of the stacks or networks. Two network names whose derived bridge subnet or routing table collide are also refused; rename one of them.

Quirks and gotchas​