Production fleet configuration reference
flowchart LR
subgraph Authority["Protected authority"]
JSON["Fleet JSON"] --> STATE["Secrets + PKI + topology"]
end
subgraph Bundles["Per-node bundles"]
STATE --> C["Control"]
STATE --> D["DNS"]
STATE --> E["Edge"]
STATE --> M["Telemetry"]
end
Copy deploy/production/examples/starter-fleet.json or multi-region-fleet.json to a protected local fleet.json, then change deployment data there. Checked-in examples are templates; repository scripts and generated Compose manifests are not configuration surfaces.
Common command options
These options work before or after a subcommand:
| Option | Default | Purpose |
|---|---|---|
--state-dir | /var/lib/cdnfoundry-fleet | Protected authoritative fleet state |
--output-dir | /var/lib/cdnfoundry-fleet/bundles | Generated per-node bundles |
--repo-root | Repository containing the script | Base production Compose file and deployment assets |
--config | none | JSON input for setup or node commands |
--non-interactive | false | Never prompt; fail when required input is absent |
--dry-run | false | Validate intent without writing state or bundles |
--yes | false | Confirm destructive or rotation operations |
The convenience wrapper uses repository-local defaults unless environment variables override them:
CDNFOUNDRY_FLEET_STATE_DIR
CDNFOUNDRY_FLEET_OUTPUT_DIRSetup config schema
{
"preset": "control-monitoring",
"global": {
"operator_domain": "ops.example.com",
"platform_domain": "example.net",
"release": "v1.2.1",
"acme_email": "operations@example.com",
"ipv6": false
},
"nodes": [],
"features": {
"monitoring": {"mode": "disabled", "host": null},
"logs": {"mode": "disabled", "host": null, "endpoint": null},
"backups": {"mode": "disabled", "repository": null, "region": "us-east-1"}
}
}Presets
| Preset | Result |
|---|---|
control-only | Control node, monitoring disabled |
control-monitoring | Control node with colocated telemetry services |
dedicated-monitoring | Control node plus a monitoring-role node |
custom | Feature configuration comes from features or later commands |
Node object
| Field | Required | Description |
|---|---|---|
name | yes | Lowercase stable identifier, letters/digits/hyphens |
role | yes | control, dns, edge, dns-edge, or monitoring |
region | yes | Routing/operations region label |
location | yes | Human-readable site label |
hostname | no | Defaults to NAME.OPERATOR_DOMAIN |
public_ipv4 | yes | Public or routed IPv4 used for inventory and policy |
public_ipv6 | no | IPv6 service address |
bind_ipv4 | no | Local listener bind, defaults to 0.0.0.0 |
bind_ipv6 | no | IPv6 bind; defaults to :: in dual-stack fleets |
monitor_ipv4 / monitor_ipv6 | no | Private monitoring addresses; IPv4 otherwise uses public_ipv4 |
log_ipv4 / log_ipv6 | no | Private log-source address metadata |
release | no | Per-node immutable override of the global release |
extra_env | no | Explicit per-node Compose overrides; always preserved in the generated .env.prod, including variables that have Compose defaults |
enabled | no | Exclude disabled nodes from rendering and targets |
draining | no | Keep node configured but remove it from preferred routing |
On setup or update-node, omitted node fields retain their saved values. Explicit JSON null clears public_ipv6, bind_ipv6, monitor_ipv4, monitor_ipv6, log_ipv4, or log_ipv6. An explicit null IPv6 bind remains disabled even in a dual-stack fleet; :: is the default only when that field is absent on a new node. Omitted --acme-email on repeated setup preserves the saved contact; an explicitly conflicting contact is still rejected.
Example:
{
"name": "pop-singapore",
"role": "dns-edge",
"region": "asia",
"location": "singapore",
"hostname": "pop-singapore.ops.example.com",
"public_ipv4": "192.0.2.40",
"public_ipv6": "2001:db8::40",
"bind_ipv4": "0.0.0.0",
"bind_ipv6": "::",
"monitor_ipv4": "10.30.0.40",
"release": "v1.2.1",
"extra_env": {},
"enabled": true,
"draining": false
}Edge registration fields
Do not put bootstrap tokens in a version-controlled setup JSON. The control plane's one-time modal is deployment-neutral and returns exactly:
EDGE_ID=11111111-2222-3333-4444-555555555555
EDGE_BOOTSTRAP_TOKEN=the-one-time-tokenPaste them into the prepared host's mode-0600 .env.prod, then start the edge profile explicitly:
sudo docker compose --env-file .env.prod --profile edge up -dAlternatively, add --profile edge to that host's generated start.sh Compose up command. Edge and Fleet node names do not have to match.
For Fleet-managed secrets, configure-edge-registration and clear-edge-bootstrap-token remain available as automation primitives. They are not a control-plane requirement. Optional host overrides such as EDGE_GATEWAY_ADDRESS_MAP, EDGE_GATEWAY_REQUIRE_ADDRESS_MAP, runtime versions, MMDB settings, and gateway capacity belong in extra_env. The address map is needed only for NAT/load-balancer translation; directly assigned service addresses bind without it. For an edge host where Docker writes invalid IP for the symbolic host-gateway, set EDGE_HOST_GATEWAY_IPV4 in that node's extra_env to the IPv4 gateway inspected from its cdnfoundry_edge network. Fleet validates a private IPv4 address and writes the explicit edge-agent.extra_hosts mapping into that node's generated Compose bundle. Reinspect the gateway if the Docker network is recreated; do not copy one host's address to another without checking.
Control database selection
Embedded PostgreSQL is used when neither DB_URL nor a non-default DB_HOST is present.
Remote PostgreSQL is selected when the control node has either:
"extra_env": {
"DB_HOST": "postgres.internal.example",
"DB_PORT": "5432",
"DB_SSLMODE": "verify-full"
}or a non-empty DB_URL.
Use set-secret --secret control-db-password --from-file FILE to replace the generated password with the remote database credential without exposing it in command arguments. In remote mode the control bundle omits control-db and its volume.
Feature objects
Monitoring
{"mode": "disabled", "host": null}disabled: no telemetry stack or node exporters.colocated: telemetry stack runs on the control node.dedicated: telemetry stack runs on the named monitoring-role node.
Logs
{"mode": "centralized", "host": "monitoring-1", "endpoint": null}disabled: no generated log collector.centralized: every enabled node receives a generated Vector config and node-specific authentication token.endpoint: optional explicit Loki-compatible URL; otherwise derived from the configured host.
Backups
{
"mode": "all-stateful",
"repository": "s3:s3.example.com/cdnfoundry-production",
"region": "us-east-1"
}Modes are disabled, control, and all-stateful.
Exit codes
| Code | Meaning |
|---|---|
0 | Success |
2 | Command-line usage error |
3 | Invalid topology, config, or failed doctor check |
4 | Missing, locked, or inconsistent fleet state |
5 | Compose, PKI, file-copy, or bundle rendering failure |
130 | Operator interrupted the command |
Generated bundle contract
Every rendered node directory includes:
.env.prod
generated Compose manifest
README.md
validate.sh
start.sh
pki/
secrets/
generated/ # when required
referenced docker/... # only runtime files used by selected servicesDNS nodes additionally receive reconcile-pdns-password.sh, its self-contained Python implementation, and a pending password file during a staged rotation.
Generated start.sh selects base profiles from the node role and reads the host's current EDGE_ID through Compose. An unenrolled dns-edge node starts DNS; after enrollment, the same script also starts edge services. An invalid UUID fails before migrations or activation. No script edit or second transfer is required. Environment files are parsed as configuration, never sourced as shell.
Security properties
- State directories use mode
0700. - State, secrets, environment files, manifests, and private keys use mode
0600at render and transfer time. During control activation, generatedstart.shchanges onlypki/edge-identity-ca.keyto ownerroot, numeric group82, mode0640, so the core image's PHP-FPM worker can read the signing key. - Node bundles are assembled in temporary directories and activated atomically.
- Normal rendering does not rotate secrets.
- DNS activation requires root and sets
docker/pdns/pdns.conftoroot:82, mode0640. PowerDNS receives supplementary group82while retaining its non-root image user. This credential-bearing file is excluded from public configuration permission repair. Rotation preserves the same ownership. - DNS database credentials are node-scoped.
- CA private keys remain in authoritative fleet state, except the edge identity CA key required by the control service in the control bundle.
- Every operator-controlled Compose interpolation value is present in the node's generated
.env.prod; production Compose provides no fallback deployment values. - Compose
environmentmappings remain explicit per-service allowlists. Replacing them with a sharedenv_fileentry would expose unrelated database, PKI, and API credentials to every container, so containers receive only the variables they own while Compose reads values through--env-file .env.prod.
Validation and repeated setup
Boolean fields accept JSON true or false, not strings or numbers. IPv4 fields require IPv4; IPv6 fields require IPv6. Preserve explicit JSON null for unused address families. extra_env secrets are redacted from status and quoted literally in generated Compose env files, including dollar signs.
setup validates the complete proposed state before writing it. Repeating setup with conflicting existing global values fails with an actionable error; it does not silently ignore a changed operator domain, platform domain, release, ACME email or address-family mode. Use the documented per-node release update for upgrades. Global DNS/PKI/topology changes require a planned migration with existing state and recovery material preserved.
Populate all required CDNF_*_IMAGE overrides from the verified manifest. A release label alone is insufficient: generated bundle validation rejects mutable image values before starting containers. Role filtering still controls which services run. Rendering stages a complete fleet generation and activates it with Linux atomic directory exchange. If a durability check fails after exchange, retain both the active and reported recovery generation and investigate; do not delete the surviving previous bundle or replace the CA.