Configuration
Cetacean reads settings from command-line flags, environment variables, and a TOML config file. When a setting appears in more than one place, the precedence order is flag, environment variable, config file, built-in default.
Sensitive settings accept a _FILE suffix on their environment variable to read the value from a
file at startup, which is how a Docker secret reaches the process. The _FILE variant ranks below
the direct environment variable.
The acl.*, mcp.*, oauth.*, sizing.*, and tracing.* settings have no command-line flags. Set them through the
environment or the config file.
A setting this page doesn’t list isn’t ignored: a config file carrying a key the schema doesn’t
know, or an environment carrying a CETACEAN_* variable no setting reads, refuses startup and
names it. So does a value a setting can’t parse: an on-off setting takes true, false, 1 or 0.
General settings
server.listen_addrAddress and port the HTTP server binds to. Use 0.0.0.0:9000 to listen on all interfaces, or 127.0.0.1:9000 to restrict to localhost.
server.base_pathURL prefix for serving Cetacean under a sub-path behind a reverse proxy, for example /cetacean. Leave unset when Cetacean owns the root path.
server.public_urlCanonical external URL clients reach Cetacean at, for example https://cetacean.example.com. Origin only—set the path prefix with server.base_path, which is used for both inbound and outbound paths. Supplies the default for oauth.issuer and auth.oidc.redirect_url, and makes Atom and JSON Feed links independent of the X-Forwarded-* headers a client can set. Set it behind a reverse proxy.
docker.hostDocker Engine API endpoint: a Unix socket (unix:///path) or TCP (tcp://host:port). Over TCP, set docker.tls.ca and a client certificate; without them nothing is encrypted, and anyone who can reach the port controls the engine. SSH (ssh://) isn’t supported and refuses startup. To reach an engine over SSH, forward its socket with ssh -L and point this at the forwarded end.
docker.tls.caPath to a PEM-encoded CA bundle the engine’s certificate must be signed by, replacing the system roots. Setting any docker.tls.* setting connects over TLS, which requires a tcp:// docker.host.
docker.tls.certPath to the PEM-encoded client certificate presented to an engine started with --tlsverify. Set it together with docker.tls.key or not at all.
docker.tls.keyPath to the PEM-encoded private key matching docker.tls.cert.
prometheus.urlBase URL of a Prometheus server, for example http://prometheus:9090. When set, Cetacean proxies metric queries and enables usage charts, sizing recommendations, and the metrics stream. When unset, every metric-dependent feature is disabled.
tracing.endpointBase URL of an OpenTelemetry collector accepting OTLP over HTTP, for example http://collector:4318. Only MCP requests and tool calls emit spans, so this needs mcp.enabled to have any effect; set without it, nothing is exported and Cetacean logs a warning. With MCP enabled, a malformed endpoint stops startup.
logging.levelMinimum log severity. One of debug, info, warn, or error.
logging.formatLog output format. json produces structured lines, text produces human-readable console output.
server.pprofExpose Go runtime profiling endpoints at /debug/pprof/ for diagnosing CPU, memory, and goroutine issues. Leave off unless you are profiling.
server.self_metricsExpose a Prometheus-compatible metrics endpoint at /-/metrics with internal counters for SSE connections, cache operations, and HTTP request latency.
server.version_checkAsk api.github.com for the latest Docker Engine release, so a node’s page can point out a newer one. Turn it off where Cetacean must not reach the internet; the node page then shows no update hint.
server.recommendationsEnable the recommendation engine, which periodically analyzes swarm state and Prometheus metrics to surface findings. See Recommendations.
server.operations_levelWhich write operations the dashboard and the API expose. 0 is read-only, 1 allows operational actions, 2 adds configuration edits, 3 allows impactful operations such as deletions and node role changes. See Operations level for the breakdown.
server.sse.batch_intervalHow long the SSE broadcaster waits to batch change events into a single push. Lower values cut UI latency, higher values cut network overhead when many resources change at once. Accepts any Go duration string, such as 50ms or 1s.
server.cors.originsOrigins allowed to make cross-origin requests to the API, as a comma-separated list, for example https://admin.example.com, or * for any. When unset, no CORS headers are sent and browsers block cross-origin requests.
The same origins are trusted for cross-origin writes; * can’t be, so with a wildcard browsers may read the API from any origin while cross-origin writes are refused.
server.trusted_proxiesComma-separated IP addresses or CIDR ranges of reverse proxies whose forwarding headers are trusted, for example 10.0.0.0/8,172.16.0.0/12. The real client IP is read from the header family server.forwarded_headers names. Required for header-based authentication, and for cert mode behind a TLS-terminating proxy. When unset, every request counts as a direct connection.
server.forwarded_headersWhich forwarding headers a proxy in server.trusted_proxies writes: x-forwarded for X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host, or forwarded for Forwarded. The other family is discarded before anything reads it.
Set this to match your proxy. Every common proxy writes the x-forwarded family by default and passes an inbound Forwarded through untouched, so a deployment reading both would let a client name its own address and origin.
storage.snapshotPersist swarm state to disk after every sync. On restart Cetacean loads the snapshot to serve data immediately while the live sync catches up.
storage.data_dirDirectory holding snapshot.json and, when the authorization server is enabled, oauth-tokens.json. The token file is written regardless of storage.snapshot. Must be writable by the Cetacean process; mount it as a volume in Docker.
tls.certPath to the PEM-encoded server certificate for TLS termination. Set it together with tls.key or not at all. Required for client certificate authentication unless a TLS-terminating proxy forwards the certificate instead; optional in every other mode.
tls.keyPath to the PEM-encoded private key matching tls.cert.
configPath to a TOML config file. See Config file for the format.
Authentication and authorization
auth.modeAuthentication provider. One of none (anonymous access), oidc, tailscale, cert (mTLS client certificates), or headers. Only one mode is active at a time. See Authentication for setup guides.
OIDC
auth.oidc.issuerrequiredBase URL of the OpenID Connect identity provider, for example https://accounts.google.com. Must support OIDC Discovery at /.well-known/openid-configuration.
auth.oidc.client_idrequiredOAuth 2.0 client ID registered with the identity provider for Cetacean.
auth.oidc.client_secretrequiredOAuth 2.0 client secret for the registered client. Accepts the _FILE suffix.
auth.oidc.redirect_urlURL the identity provider redirects to after authentication, for example https://cetacean.example.com/auth/callback. Must use HTTPS; http://localhost and http://127.0.0.1 are allowed for local development. Defaults to {server.public_url}{server.base_path}/auth/callback; required only when server.public_url is unset, or when the callback lives elsewhere.
auth.oidc.scopesComma-separated OAuth 2.0 scopes to request. The defaults cover user identity, display name, and email. Add provider-specific scopes for further claims, such as group membership.
auth.oidc.session_keyHex-encoded 32-byte HMAC key signing browser session cookies. When unset, a random key is generated at startup and restarting invalidates all sessions. Generate a fixed one with openssl rand -hex 32. Accepts the _FILE suffix.
Tailscale
auth.tailscale.modeHow Cetacean reaches Tailscale. local queries the host’s Tailscale daemon. tsnet embeds a Tailscale node in the process, which suits containers. See Authentication for a comparison.
auth.tailscale.authkeyTailscale auth key for automatic node enrollment, required in tsnet mode. Generate one in the Tailscale admin console under Settings, Keys. Accepts the _FILE suffix.
auth.tailscale.hostnameHostname the embedded Tailscale node registers as (tsnet mode only). The dashboard is then reachable at https://cetacean.[[tailnet-name]].ts.net.
auth.tailscale.state_dirDirectory for persisting tsnet node state (keys, registration). Mount it as a volume in Docker to avoid re-enrolling the node on every restart.
auth.tailscale.capabilityTailscale ACL capability key mapping tailnet users to application groups, for example example.com/cap/cetacean. Cetacean reads group grants from Tailscale’s CapMap into the identity’s groups array for authorization.
Client certificates
auth.cert.carequiredPath to a PEM-encoded CA certificate bundle. Client certificates must be signed by one of its CAs. Supports certificate chains and multiple CAs in one file. Configures Cetacean’s own TLS listener, so it requires tls.cert and tls.key and is required with them. Behind a TLS-terminating proxy the proxy verifies the certificate against its own CA instead, and this setting isn’t consulted.
Trusted proxy headers
In headers mode Cetacean trusts whatever the proxy sends, so server.trusted_proxies
must be set. Without it any client that can reach Cetacean directly can claim any identity.
auth.headers.subjectrequiredName of the HTTP header carrying the user’s unique identifier, for example X-Remote-User. Becomes the identity’s subject and is used for authorization audience matching.
auth.headers.nameName of the HTTP header carrying the user’s display name, for example X-Remote-Name. Shown in the dashboard’s user badge.
auth.headers.emailName of the HTTP header carrying the user’s email address, for example X-Remote-Email. Used for authorization audience matching alongside the subject.
auth.headers.groupsName of the HTTP header carrying group memberships as a comma-separated list, for example X-Remote-Groups. Used for authorization audience matching.
auth.headers.secret_headerName of an HTTP header that must carry a shared secret for the request to be accepted. Optional protection on top of the IP check in server.trusted_proxies.
auth.headers.secret_valueExpected value of the shared secret header. Required when auth.headers.secret_header is set. Accepts the _FILE suffix.
Authorization
Authorization is grant-based RBAC layered on the active authentication mode. With no policy
configured every authenticated user has full access; once a policy is present the default is deny.
Auth mode none bypasses authorization. See Authorization for the grant syntax.
acl.policyInline authorization policy as JSON, YAML, or TOML. Suitable for a short policy passed through the environment. Prefer acl.policy_file for anything longer, since only a file is watched for changes.
acl.policy_filePath to a policy file (JSON, YAML, or TOML). The file is watched and reloaded on change, so grants can be edited without restarting Cetacean.
The following three settings read grants carried by the identity itself. They add to whatever the policy already grants, and each applies only to its own authentication mode.
acl.tailscale_capabilityTailscale CapMap key holding per-user grants, for example example.com/cap/cetacean-acl. Read in tailscale mode only.
acl.oidc_claimName of the OIDC token claim holding grants, for example cetacean_grants. Read in oidc mode only.
acl.headers_aclName of the HTTP header holding grants as JSON, for example X-Remote-Grants. Read in headers mode only, and only from a request that passed the server.trusted_proxies check.
acl.headers_capabilityKey of the grants array when the acl.headers_acl header holds a JSON object rather than an array, for example example.com/cap/cetacean for the Tailscale-App-Capabilities header tailscale serve sends. A missing key or a value of the wrong shape grants nothing. Unset, the header must be a bare array.
OAuth authorization server
Cetacean can run an OAuth 2.1 authorization server, which issues the bearer tokens clients present to protected resources. It’s disabled by default and has to be asked for: it mints credentials for your cluster, so it should never appear as a side effect of enabling something else.
Two resources can be protected: /mcp, and the web API when oauth.api_tokens is on. /mcp
authenticates itself rather than going through the authentication middleware. So under an auth
mode other than none it needs one of two things, and Cetacean refuses to start with neither: this server, or the
active mode listed in
mcp.auth_bypass, which has the upstream provider authenticate every request instead. The
bypass is how an mTLS deployment runs no authorization server at all—those clients can’t drive a browser
consent screen, so one would only sit unused.
oauth.enabledRun the authorization server, mounting /oauth/* and the RFC 8414 and RFC 9728 discovery documents. Requires an auth mode other than none, since the server has no identity of its own to consent as.
oauth.issuerCanonical external base URL, for example https://cetacean.example.com. Becomes the OAuth issuer identifier and the base of the resource audience, so it must be the URL clients reach. Defaults to server.public_url; set this only to give the authorization server a different URL from the rest of Cetacean. With neither set, it’s derived from the listen address, and startup fails if that yields no reachable host.
oauth.signing_keyRoot secret the token-signing key and the consent page’s CSRF key are each derived from. When unset, a fresh key is generated at startup and every existing token becomes invalid, so set it for any deployment that should survive a restart. Generate one with openssl rand -hex 32. Cetacean refuses to start unless the value decodes as hex or base64 to exactly 32 bytes: the public half of the signing key is published and unauthenticated, so a guessable root could be recovered from it. Accepts the _FILE suffix.
oauth.access_token_ttlHow long an access token stays valid.
oauth.refresh_token_ttlHow long a refresh token stays valid (30 days). Refresh tokens rotate single-use and persist under storage.data_dir, so clients keep working across restarts.
oauth.consent_ttlHow long a remembered approval lets a client skip the consent screen (90 days). Keep it above oauth.refresh_token_ttl, or an expiring refresh token costs a second prompt. Set 0s to prompt on every authorization.
oauth.api_tokensOffer the web API as a protected resource, so a client can obtain a token for it and authenticate with Authorization: Bearer instead of a browser session. The API and /mcp are separate resources: a token for one is refused by the other, whichever path it sits under. Turning this off makes the API resource undiscoverable as well as unusable—there’s no metadata document for it, and the authorize endpoint refuses to mint one.
oauth.token_operations_levelWhat a caller holding a token may do on the web API: 0 reads only, 1 operational, 2 configuration, 3 impactful. Inherits server.operations_level when unset, and narrows it rather than replacing it—a token never reaches past the tier the deployment itself runs at. Set it lower to leave a token on a device that can read the cluster but not change it. /mcp is governed by mcp.operations_level instead: MCP registers its tool list once, so it can’t hold two callers to different tiers.
oauth.require_resource_indicatorRequire the RFC 8707 resource parameter on authorize and token requests, which stops a token issued for Cetacean being replayed against another server and is how a client says which resource it wants. Turn it off only for a client that can’t send the parameter; a request without one is then bound to the web API, or to /mcp where oauth.api_tokens is off.
oauth.dcr_enabledAccept RFC 7591 Dynamic Client Registration at /oauth/register. Registrations are kept across a restart, beside the refresh tokens in oauth-tokens.json. MCP revision 2026-07-28 deprecates DCR in favor of Client ID Metadata Documents, and it stays on for clients that don’t support those yet.
oauth.dcr_rate_limitMaximum dynamic client registrations accepted per IP address per hour.
oauth.dcr_max_clientsGlobal cap on registered clients. Once reached, the oldest registration is evicted, and the eviction is logged.
oauth.cimd_enabledAccept Client ID Metadata Documents, where a client identifies itself by an https:// URL Cetacean fetches. Fetches pass an SSRF blocklist and are cached as the response’s Cache-Control or Expires allows, for at most a day, or an hour when it says nothing.
MCP server
Cetacean can expose cluster data to AI agents over the Model Context Protocol. The server is disabled by
default. When enabled it mounts at /mcp and requires an OAuth 2.1 bearer token from the
authorization server, unless the active auth mode is listed in
mcp.auth_bypass. Every tool call is also checked against the
authorization grants, so an agent gets no more access than the calling identity already has.
mcp.enabledEnable the embedded MCP server at /mcp.
mcp.operations_levelWhich tools MCP clients may call: 0 reads only, 1 operational, 2 configuration, 3 impactful. Inherits server.operations_level when unset, and narrows it rather than replacing it—a value above the global one grants nothing. Set it lower to allow writes from the dashboard that agents can’t make. This is the ceiling for every MCP client, whatever it authenticated with; oauth.token_operations_level governs the web API.
mcp.max_concurrent_tasksCap on in-flight task-augmented tool calls. Mutations that wait for a service to converge run as tasks, so this bounds how many such waits can be outstanding.
mcp.task_ttlHow long a task’s result is retained when the call omits a TTL of its own. Set 0s to disable the fill-in, which then keeps such results for the life of the process.
mcp.max_task_ttlCeiling on the retention a call may ask for; longer requests are clamped to it. Set 0s to accept whatever a client asks for.
mcp.auth_bypassComma-separated authentication modes that skip the bearer check at /mcp, for example cert. Use it where the transport already carries a verified identity. Authorization still applies; only the OAuth handshake is skipped. A mode listed here is authenticated by its own provider on every request, so it satisfies /mcp on its own and oauth.enabled can stay off. Only cert, headers, and tailscale may be listed, and Cetacean refuses to start on anything else: those carry identity in the transport, where oidc authenticates from a session cookie—an ambient credential that /mcp’s exemption from cross-origin protection assumes is absent.
See MCP for the tools, resources, prompts, and widgets the server exposes.
Recommendation sizing
These tune the resource right-sizing checker, the only recommendation category with configurable thresholds. It requires Prometheus, since it compares reservations and limits against measured usage. The other checkers use fixed thresholds.
sizing.headroom_multiplierMultiplier applied to observed usage when suggesting a new reservation or limit. Accepts 1.0 to 10.0.
sizing.thresholds.over_provisionedUsage below this fraction of the reservation marks a service over-provisioned. Accepts 0.0 to 1.0.
sizing.thresholds.approaching_limitUsage above this fraction of the limit raises a warning. Accepts 0.0 to 1.0.
sizing.thresholds.at_limitUsage above this fraction of the limit raises a critical finding. Accepts 0.0 to 1.0. Startup refuses
thresholds out of order: over_provisioned ≤ approaching_limit ≤ at_limit.
sizing.thresholds.lookbackWindow over which p95 usage is queried when deciding whether a service is over-provisioned (7 days). A short window makes the check react to a quiet weekend; a long one needs that much history in Prometheus.
Config file
Pass a TOML file with -config or CETACEAN_CONFIG. Without either, Cetacean reads the first
of these that exists: ./cetacean.toml, $XDG_CONFIG_HOME/cetacean/cetacean.toml (or
~/.config/cetacean/cetacean.toml), ~/.cetacean.toml, and /etc/cetacean/cetacean.toml. Every
field is optional; an omitted key uses the default.
CETACEAN_CONFIG=/etc/cetacean/config.toml cetacean
Without either, Cetacean loads the first of these that exists: ./cetacean.toml,
$XDG_CONFIG_HOME/cetacean/cetacean.toml (or ~/.config/cetacean/cetacean.toml), ~/.cetacean.toml, and
/etc/cetacean/cetacean.toml. The startup log names the file it loaded and whether it was discovered.
docs/config.reference.toml in the repository lists every option with its default. Copy it as a
starting point and uncomment what you need.
Subcommands
cetacean Start the server
cetacean healthcheck Exit 0 if ready, 1 otherwise (for Docker HEALTHCHECK)
Snapshots
Cetacean writes all cached swarm state to ${data_dir}/snapshot.json after every sync. On startup
it loads the snapshot to serve stale-but-fast data while the live sync catches up, in every
representation: JSON, CSV, MCP, and the dashboard alike. Without a snapshot, reads of cluster
state answer 503 until the first sync, and /-/ready answers 503 until then either way.
Snapshot writes are atomic, so a crash mid-write can’t corrupt the file.
Health checks
Two meta endpoints, exempt from authentication and content negotiation:
| Endpoint | Behavior | Use for |
|---|---|---|
GET /-/health | Always 200 if the process is running | Uptime monitoring, restart policies |
GET /-/ready | 200 after the first sync that reaches the swarm; 503 until then | Load balancers, Docker health checks |
GET /-/health also reports the version, the operations level, and whether Cetacean is still tracking the cluster;
the API page has the fields.
The cetacean healthcheck subcommand probes /-/ready and is what the built-in Docker
HEALTHCHECK runs. It loads its configuration the way the server does, so it probes the
configured listen address, base path, and TLS setting. Docker runs it as a separate process
without the service’s command-line flags, though: set anything it must honor in the environment,
in a file named by CETACEAN_CONFIG, or in a file at one of the discovered paths.
Flags given to the subcommand itself go before healthcheck.
Operations level
server.operations_level sets the ceiling on which write operations the dashboard and the API
expose. Each level includes everything below it. A request above the configured level gets
403 Forbidden with error code OPS001.
| Operation | 0 read-only | 1 operational | 2 configuration | 3 impactful |
|---|---|---|---|---|
| Browse all resources | ✔ | ✔ | ✔ | ✔ |
| Scale, restart, roll back, update image | — | ✔ | ✔ | ✔ |
| Edit service definitions (env, labels, resources, ports, placement, policies) | — | — | ✔ | ✔ |
| Attach configs, secrets, networks, volumes, and tmpfs mounts to a service | — | — | ✔ | ✔ |
| Edit node labels | — | — | ✔ | ✔ |
| Create configs and secrets, edit their labels | — | — | ✔ | ✔ |
| Enable, disable, or configure plugins | — | — | ✔ | ✔ |
| Edit swarm settings (raft, orchestration, dispatcher) | — | — | ✔ | ✔ |
| Delete any resource | — | — | — | ✔ |
| Add a bind or named-pipe mount, a volume driver, or a capability to a service | — | — | — | ✔ |
| Change node availability or role | — | — | — | ✔ |
| Change a service’s endpoint mode | — | — | — | ✔ |
| Install, upgrade, or remove plugins | — | — | — | ✔ |
| Manage CA and encryption, read and rotate join tokens, unlock the swarm | — | — | — | ✔ |
Operations level and grants are independent checks
and both must pass. Operations level is the global ceiling, grants control per-user scope. Set
mcp.operations_level to give MCP clients a lower ceiling than the dashboard, and
oauth.token_operations_level to do the same for API tokens.
Profiling
When server.pprof is enabled, the standard Go profiling endpoints are available at
/debug/pprof/:
go tool pprof http://localhost:9000/debug/pprof/profile # 30s CPU profile
go tool pprof http://localhost:9000/debug/pprof/heap # heap snapshot