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.0.0",
"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 |
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.0.0",
"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.
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 may additionally receive reconcile-pdns-password.sh and a pending password file during a staged rotation.
Generated start.sh contains a fixed list of Compose profiles. A combined dns-edge bundle rendered before enrollment starts only DNS. After an operator pastes EDGE_ID and EDGE_BOOTSTRAP_TOKEN into .env.prod, start the edge profile explicitly or add --profile edge to the script's Compose up command. The script does not inspect enrollment state or modify .env.prod.
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 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.