Skip to main content

Stack spec

A stack is one YAML file that describes a group of containers (called services) for Clouduler to run across your cluster. This page lists every field the file accepts. It is a subset of Docker Swarm / Compose v3 plus an x-clouduler block for things Compose has no concept of: reverse-proxy routes, drive-backed volumes, node pins, actions and alert rules.

For the task-oriented view, see Stacks. Terms like drive, overlay and placement are explained in the glossary.

Three rules to know before the tables:

  • Unknown keys are an error. A typo fails the parse instead of being ignored, and build: is not supported.
  • Everything takes effect on redeploy. The spec is stored as text. A change applies when you deploy a new revision, so the Reload column below reads n/a for every field.
  • Types. duration is a humantime value (30s, 2m, 1h30m). size is a byte size (10MB, 64MiB, 512M, or a plain number of bytes).

How validation works​

Validation runs in two layers.

  1. Syntax runs on every save in the web UI and on every deploy. It needs no cluster: names, shapes, port collisions, dependency cycles, and so on.
  2. Cluster checks ask whether the network, drive and node you named actually exist and whether the service can be placed. The web UI, the HTTP API (and the MCP tools), the CLI (clouduler stack deploy) and the git-backup pull all run them, and refuse the deploy with a message when a drive, network or node pin does not exist.

Because the UI errors come back without a path, the message carries the whole location, for example invalid config at services.web.env_file: ..., and the editor does not highlight a line for it.

A full example​

This file touches most of the common fields. Comments explain each part.

gitlab.yaml
version: "3.8" # ignored, optional

networks:
wan: {} # a cluster network with a domain, used by the proxy route
tailscale: {} # a second network, used by the SSH expose

secrets:
db_password:
file: ./secrets/db.txt # read at deploy time, sealed with the cluster key
configs:
gitlab_rb:
file: ./conf/gitlab.rb # bundled at deploy time

services:
gitlab:
image: gitlab/gitlab-ce:16.9.1-ce.0
restart: unless-stopped # the real restart policy lives here
networks: [wan, tailscale] # must be declared above
environment:
GITLAB_OMNIBUS_CONFIG: "external_url 'https://gitlab.example.com'"
volumes:
- fast/gitlab-data:/var/opt/gitlab # drive/volume:/target
- fast/gitlab-config:/etc/gitlab
- bulk/gitlab-logs:/var/log/gitlab
- type: tmpfs
target: /tmp/cache
tmpfs: { size: 64m }
secrets:
- source: db_password
env: DB_PASSWORD # exposed as an env var instead of a file
configs:
- source: gitlab_rb
target: /etc/gitlab/gitlab.rb
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/-/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 2m
stop_grace_period: 2m
deploy:
replicas: 1
resources:
limits: { cpus: "2.0", memory: 4G }
x-clouduler:
node: atlas # hard pin, optional
proxy:
- host: gitlab # becomes gitlab.<domain of wan>
network: wan
port: 80
tls: true
middlewares: [redirect_https]
max_body_size: 2GiB
preserve_host: true
response_timeout: 120s
expose:
- port: 22 # container port
published: 2222 # host port, bound on every node
protocol: tcp
network: tailscale
backup:
stop_before: true # stop the service during a restic backup

Top level​

Option
Type / default
Reload
Description
strno default
n/a
Compose schema version. Accepted and ignored.
servicesrequired
mapno default
n/a
The containers of the stack, keyed by service name. Each key must be a DNS label.
mapdefault {}
n/a
Cluster networks this stack uses. Clouduler does not create Docker networks from this block. Each name must exist in the cluster and be implemented by some node (nat networks excepted).
list of strdefault unrestricted
n/a
Restricts what the stack can reach through that network. Entries: host, *.suffix, host:port, host:lo-hi, host:port/udp, or an IPv4 literal. See Networking.
Watch out: A bare allowed: with no value is an error. Write allowed: [] to deny everything.
mapdefault {}
n/a
Accepted for Compose compatibility but not used. Services reference drives directly.
Watch out: Parsed but currently not acted on.
mapdefault {}
n/a
Cluster configs. With file: the content is bundled at deploy time (path relative to the stack file, or absolute). Without file: the config must already exist in the cluster.
mapdefault {}
n/a
Cluster secrets. Same rules as configs. The content is sealed with the cluster key.
mapno default
n/a
Stack-wide defaults that services inherit: auto_update, node, volume_sync, backup, alerts. Other keys are rejected here.
Watch out: proxy, expose, colocate, actions and docker_socket are service-level only.

Stack names and service names​

The stack name is not a spec key. It comes from the file name (CLI, without the extension) or the Stack name field in the web UI. It must be 1 to 63 bytes, start and end with a lowercase letter or digit, and use only a-z, 0-9, - and _. The UI hint says "Lowercase letters, numbers and dashes", but the server also accepts underscores.

Each key under services: must be a DNS label (lowercase letters, digits and hyphens, not starting or ending with a hyphen, at most 63 characters). It is the service's hostname on the overlay and part of the container name STACK_SERVICE_INDEX.

Service​

Option
Type / default
Reload
Description
imagerequired
strno default
n/a
Container image reference.
Watch out: build: is rejected as an unknown key.
strno default
n/a
Container hostname. Independent of the service name.
string or listdefault image default
n/a
Overrides the image CMD. A string is split like Docker does, a list is exec form.
string or listdefault image default
n/a
Overrides the image ENTRYPOINT.
map or list of KEY=valueno default
n/a
Environment variables.
Watch out: List entries must contain =. Numbers and booleans in a map are stringified, null becomes empty. Secret environment variables come from secrets with env:.
path or listno default
n/a
Not supported.
Watch out: Always rejected. Move the entries into environment.
map or list of key=valueno default
n/a
Docker labels on the container. Clouduler adds its own revision label.
listno default
n/a
Mounts from drives, or in-memory tmpfs. See Volumes below.
listno default
n/a
Ports published on the node that runs the container. See Ports below.
list of namesno default
n/a
Networks this service attaches to. Each must be declared in the stack's top-level networks.
Watch out: At most one egress, wireguard or nat network per service.
list of service namesno default
n/a
Start-order gate: an instance is not first created until every dependency has at least one instance running on any node.
Watch out: Waits for running, not healthy. Cycles and self-references are errors.
mapno default
n/a
Docker healthcheck. See Healthcheck below.
Watch out: Without test: the whole block is silently dropped.
strdefault no
n/a
Docker restart policy: no, always, on-failure, on-failure:N, unless-stopped.
strdefault image default
n/a
Process user: uid or uid:gid.
strdefault image default
n/a
Working directory.
list of strno default
n/a
Extra Linux capabilities.
list of strno default
n/a
Dropped capabilities.
map or list of key=valueno default
n/a
Kernel parameters for the container.
booldefault false
n/a
Read-only root filesystem. Writes outside declared volumes fail.
durationdefault cluster failover.stop_grace (30s)
n/a
How long a graceful stop may take before the kill.
Watch out: Must be greater than zero and at most 600s.
strdefault image STOPSIGNAL
n/a
Signal for the graceful stop, for example SIGINT.
Watch out: Must be SIG plus uppercase letters, or digits. Lowercase sigint fails.
listno default
n/a
Cluster configs mounted into this service. Name or long form, see Configs and secrets in services.
listno default
n/a
Cluster secrets mounted at /run/secrets/NAME. Name or long form.
Watch out: write: true on a secret is rejected.
mapno default
n/a
Replicas, placement, resources. See Deploy.
mapno default
n/a
Clouduler extensions for this service. See Proxy and x-clouduler per service.

Volumes​

Volumes come from drives, from tmpfs, or are anonymous container paths. Host binds are not supported. See Drives and volumes.

Option
Type / default
Reload
Description
strno default
n/a
Mount a volume from a drive, read-write. The reference is split on the first slash.
strdefault rw
n/a
Read-only mount. Mode must be ro or rw.
strno default
n/a
Anonymous container path with no drive behind it.
strno default
n/a
Mount the drive's whole root directory.
Watch out: Allowed only when the drive has exactly one holder and the service is pinned to it with x-clouduler.node. See Drives and volumes.
strno default
n/a
Long form. Same syntax as the short form source.
targetrequired
absolute pathno default
n/a
Long form. Container path.
booldefault false
n/a
Long form. Read-only mount.
strno default
n/a
Long form. In-memory mount. Contents are lost on restart and never replicated.
Watch out: A tmpfs cannot have a source.
sizedefault Docker default
n/a
Size cap of the tmpfs, for example 64m.
n/ano default
n/a
Not supported.
Watch out: Bind mounts and host paths are rejected: volumes must live on a drive so they can be replicated and migrated.

Ports​

Option
Type / default
Reload
Description
strno default
n/a
Container port only, nothing published.
strno default
n/a
Publish on the node that runs the container. Ranges like 8080-8090:80-90 are allowed when both sides are ranges of equal length.
strno default
n/a
Bind to a specific host address.
strdefault tcp
n/a
Suffix /tcp or /udp selects the transport.
targetrequired
int or rangeno default
n/a
Long form. Container port.
int or rangeno default
n/a
Long form. Host port.
IPno default
n/a
Long form. Bind address.
tcp or udpdefault tcp
n/a
Long form. Transport.
host or ingressdefault host
n/a
Long form. Only host is supported.
Watch out: ingress is rejected: Clouduler has no Swarm-style ingress mesh. Use x-clouduler.expose for a port that follows the container across nodes.

A published port is bound on the node that runs the container and moves with it. For a port that follows the container across nodes, use x-clouduler.expose. Two services in the same stack cannot publish the same port. Collisions between different stacks are not checked.

Healthcheck​

Option
Type / default
Reload
Description
testrequired
string or listno default
n/a
CMD, CMD-SHELL, NONE list forms, or a shell string.
Watch out: If absent, the whole healthcheck block is ignored.
durationdefault Docker default
n/a
Time between checks.
durationdefault Docker default
n/a
Per-check timeout.
durationdefault Docker default
n/a
Grace period before failures count.
intdefault Docker default
n/a
Failures before the container is unhealthy.
booldefault false
n/a
true removes the healthcheck entirely.

Configs and secrets in services​

Option
Type / default
Reload
Description
strno default
n/a
Mount the object at its default path.
Watch out: An empty name is an error.
sourcerequired
strno default
n/a
Name of the cluster config or secret.
absolute pathdefault secrets: /run/secrets/NAME, configs: /NAME
n/a
Where it appears in the container.
octaldefault 0444 (writable configs 0644)
n/a
File permissions. 0444 written unquoted works.
strno default
n/a
File owner.
strno default
n/a
Expose the value as an environment variable instead of a file.
Watch out: Cannot be combined with target, mode, uid, gid or write.
booldefault false
n/a
Configs only. Container edits feed back to the cluster's copy.
Watch out: Only one service may hold a writable config, and not with replicas above 1. A secret with write: true is rejected.

Deploy​

Option
Type / default
Reload
Description
intdefault 1
n/a
Number of instances, named STACK_SERVICE_INDEX. 0 is allowed.
Watch out: At most 1000. Ignored when mode is global. A service with more than one replica cannot hold a writable config.
replicated or globaldefault replicated
n/a
global runs one instance on every eligible node.
list of strno default
n/a
Swarm-style node filters: KEY==VALUE or KEY!=VALUE with keys node.hostname, node.id, node.role (manager or worker), node.labels.KEY.
Watch out: node.hostname==NAME and node==NAME are checked against cluster nodes by the CLI. An unknown key never matches.
str (float)no default
n/a
CPU cap, for example "1.0".
Watch out: A value that does not parse is silently ignored.
sizeno default
n/a
Memory cap. Also used by the rebalancer.
Watch out: An unparseable value is silently ignored (no cap). A digit string that overflows is an error.
same as limitsno default
n/a
Parsed but currently not acted on. No consumer outside the config parser.
mapno default
n/a
Parsed but currently not acted on. Use the service-level restart key instead.
Watch out: The shipped gitlab.yaml example sets max_attempts: 3 here and it does nothing.
mapno default
n/a
Parsed but currently not acted on. Rolling updates are not controlled from the spec.

Proxy and x-clouduler per service​

x-clouduler is the Clouduler-specific block on a service.

Option
Type / default
Reload
Description
strno default
n/a
Hard pin to one node. Must be a cluster node.
Watch out: Inherited from the stack-level default. A stack-wide node can break drive/. whole-drive mounts.
list of servicesno default
n/a
Pins this group of services to the same node. Transitive.
Watch out: Must name other services in the stack, not itself.
list of routesno default
n/a
HTTP routes published through the reverse proxy. See the proxy table below.
listno default
n/a
Non-HTTP ports routed over the overlay (SSH and similar). See the expose table below.
strno default
n/a
Name of an action of this service, run in the container on the primary node before each scheduled replication. If it fails, that replication is skipped.
Watch out: Service level only. Runs on scheduled replication, not on migrations or manual syncs.
strno default
n/a
Name of an action run after each replication attempt.
Watch out: Service level only.
durationdefault drive sync_interval
n/a
Per-volume replication interval override. Parsed but currently not acted on.
Watch out: Only the drive's own sync_interval is used today.
booldefault true
n/a
Per-volume replication on/off. Parsed but currently not acted on.
booldefault false
n/a
Meant to follow :latest. Parsed but currently not acted on.
Watch out: Image updates are applied from the Updates page.
booldefault drive backup target decides (stop)
n/a
Whether a restic backup stops this service first.
pathno default
n/a
Path inside the container where the Docker socket appears.
local or cloudulerdefault local
n/a
local is the node's real socket (root-equivalent on the host). clouduler is a read-only endpoint for image queries.
booldefault false
n/a
Read-only socket.
Watch out: mode: clouduler requires true.
mapno default
n/a
Named commands runnable in the container from the UI or as sync hooks. The label is the button text.
str or listno default
n/a
Command. A string runs as sh -c, a list is exec form.
strdefault container user
n/a
Run as this user.
durationdefault 30s
n/a
Time limit.
Watch out: Capped at 300s: longer values are clamped.
booldefault false
n/a
The UI asks before running.
booldefault true
n/a
false hides the button and refuses UI/API runs. The action is then only usable as a sync hook.
false or mapdefault enabled
n/a
Alert control for the service's containers, merged over the stack-level block. See Alerts below.

Proxy routes​

Each entry of x-clouduler.proxy. See Proxy and TLS.

Option
Type / default
Reload
Description
hostrequired
strno default
n/a
Hostname of the route. A bare label gets the network's domain appended, a name with a dot is used as is.
networkrequired
strno default
n/a
Network that publishes the route. Must be declared in the stack and have a domain in the cluster.
portrequired
intno default
n/a
Container port the proxy forwards to.
strno default
n/a
The route only matches under this prefix.
booldefault true
n/a
HTTPS on the route.
listno default
n/a
Behaviours layered on the route. See Middlewares.
boolno default
n/a
Session affinity.
sizedefault unlimited
n/a
Upload cap. The proxy answers 413 above it.
booldefault true
n/a
Allow Upgrade requests.
booldefault false
n/a
Forward the client's Host header.
durationdefault none (waits forever)
n/a
Maximum wait for the backend's response headers.
booldefault false
n/a
Publish the hostname through Cloudflare's proxy. Public networks only; on only if every route behind the host sets it.

Expose​

Each entry of x-clouduler.expose. See Networking.

Option
Type / default
Reload
Description
portrequired
intno default
n/a
Container port.
publishedrequired
intno default
n/a
Host port bound on every node. Must not collide with ports in the same stack.
tcp or udpdefault tcp
n/a
Transport.
networkrequired
strno default
n/a
Declared cluster network. On a public or wireguard network, some node must list the port in open_ports.
Watch out: Error: no node has PORT/PROTO open on network NET, this port could never be reached.
booldefault false
n/a
Send a PROXY v2 header so the backend sees the real client address. TCP only.
Watch out: Enable PROXY v2 on the backend first or the connection breaks.

Middlewares​

Written under middlewares: of a route, either as a bare name (redirect_https) or as a one-key map (cors: {origins: [...]}).

Option
Type / default
Reload
Description
baredefault
n/a
Answer plain HTTP with a redirect to HTTPS.
baredefault
n/a
Remove the route's path_prefix before forwarding.
bare or mapdefault
n/a
Require an IdP login. Needs proxy.auth in the cluster config covering that network.
Watch out: Error: authenticated asks for a login on network NET, but proxy.auth does not list it in networks.
listdefault any logged-in user
n/a
Restrict by IdP group. Empty names are rejected.
booldefault true
n/a
Let containers on the cluster's own networks through without a login.
mapno default
n/a
Set headers on the request to the container or the response to the client.
Watch out: Header names must be valid tokens, values cannot contain control characters. A bare headers entry is rejected: use a map.
listno default
n/a
Headers to remove.
cors.originsrequired
listno default
n/a
Allowed origins.
Watch out: * cannot be combined with allow_credentials.
listdefault GET HEAD POST PUT PATCH DELETE OPTIONS
n/a
Allowed methods.
listdefault echo the request
n/a
Allowed request headers.
booldefault false
n/a
Allow credentials.
durationno default
n/a
Preflight cache time.
strno default
n/a
Matched against the full URL including host and query. Must compile.
strno default
n/a
Replacement with $1, $2 from capture groups.
booldefault true
n/a
true gives 301/308, false gives 302/307.

Alerts​

Allowed at the stack level and the service level under x-clouduler.alerts. See Alerts.

Option
Type / default
Reload
Description
booldefault true (enabled)
n/a
Nothing about this stack or service is sent. Log rules still parse but never run.
listno default
n/a
Alert keys like instance.unhealthy, or a prefix like instance.*, to mute.
info, warning or criticaldefault per event
n/a
Override the severity of an event.
Watch out: An unknown event key is rejected.
strno default
n/a
Plain substring to look for in the logs. Exactly one of match and regex.
strno default
n/a
Rust regex.
booldefault false
n/a
Case-insensitive match.
enumdefault warning
n/a
Severity of the alert.
strdefault SERVICE logged "PATTERN"
n/a
Alert title.
durationdefault 5m
n/a
After a match, repeats inside the window are counted, not raised.
both, stdout or stderrdefault both
n/a
Which stream to watch.
listdefault all
n/a
Stack level only. Limits a stack-level rule to named services.

Common validation errors​

The format is invalid config at PATH: MESSAGE, where PATH is a dotted path such as services.web.volumes[0].

Syntax errors (UI and CLI)

  • service name must be a valid DNS label (lowercase letters, digits, hyphens, not starting or ending with a hyphen) since it becomes an overlay hostname
  • env_file is not supported yet, move its entries into environment
  • network NET is not declared in this stack's top-level networks
  • no service named DEP in this stack, a service cannot depend on itself, dependency cycle: a -> b -> a
  • port P/PROTO is already published by services.X.ports[I]
  • bind mounts are not supported, volumes must live on a drive so they can be replicated and migrated
  • "SRC" must use drive/volume syntax, e.g. fast/gitlab-data
  • mode: ingress is not supported, clouduler has no swarm-style ingress mesh; ...
  • unknown restart policy "X", expected no, always, on-failure[:N] or unless-stopped
  • "sigint" is not a signal name (e.g. SIGINT) or number
  • must be greater than zero or must be at most 600s (stop_grace_period)
  • write is not allowed on a secret, only on a config
  • CFG is writable but this service runs N replicas; only one instance may write a config
  • N is more than the maximum of 1000 (replicas)
  • expected KEY=value, got "ENTRY" (list-form environment or labels)
  • mode: clouduler is read-only; set read_only: true
  • allowed must be a list (write `allowed: []` to deny everything, or drop the key)
  • invalid stack name "NAME": must be 1 to 63 bytes long, must start and end with a lowercase letter or digit, only lowercase letters, digits, - and _ are allowed

Cluster errors (CLI and git sync only)

  • no network named NET declared in the cluster
  • no node implements network NET, this service could never be placed
  • only one egress network is allowed per service, found A and B
  • no drive named DRIVE declared in the cluster
  • no node named NODE in the cluster
  • network NET has no domain, so it cannot serve an HTTP route
  • no node has P/PROTO open on network NET, this port could never be reached
  • drive D can be served by more than one node (...), so mounting D/. (its whole root) is not allowed: pin a specific volume under it instead

Deploy errors (UI and API)

  • config NAME needs content: it has no existing value to keep (and the same for a secret)
  • this node holds no cluster key (the node you are connected to cannot seal secrets)
  • stacks A and B would both use the overlay network ... which would break isolation between them: rename one of the stacks or networks

Deploy errors (CLI)

  • this stack has secrets, so the cluster key at PATH is needed. Deploy from a node that holds it, or use the web ui
  • NAME is N bytes, over the 1048576 byte limit (each bundled config or secret file is capped at 1 MiB)

Quirks and gotchas​