Skip to content

Production fleet configuration reference ​

Diagram source
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:

OptionDefaultPurpose
--state-dir/var/lib/cdnfoundry-fleetProtected authoritative fleet state
--output-dir/var/lib/cdnfoundry-fleet/bundlesGenerated per-node bundles
--repo-rootRepository containing the scriptBase production Compose file and deployment assets
--confignoneJSON input for setup or node commands
--non-interactivefalseNever prompt; fail when required input is absent
--dry-runfalseValidate intent without writing state or bundles
--yesfalseConfirm destructive or rotation operations

The convenience wrapper uses repository-local defaults unless environment variables override them:

text
CDNFOUNDRY_FLEET_STATE_DIR
CDNFOUNDRY_FLEET_OUTPUT_DIR

Setup config schema ​

json
{
  "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 ​

PresetResult
control-onlyControl node, monitoring disabled
control-monitoringControl node with colocated telemetry services
dedicated-monitoringControl node plus a monitoring-role node
customFeature configuration comes from features or later commands

Node object ​

FieldRequiredDescription
nameyesLowercase stable identifier, letters/digits/hyphens
roleyescontrol, dns, edge, dns-edge, or monitoring
regionyesRouting/operations region label
locationyesHuman-readable site label
hostnamenoDefaults to NAME.OPERATOR_DOMAIN
public_ipv4yesPublic or routed IPv4 used for inventory and policy
public_ipv6noIPv6 service address
bind_ipv4noLocal listener bind, defaults to 0.0.0.0
bind_ipv6noIPv6 bind; defaults to :: in dual-stack fleets
monitor_ipv4 / monitor_ipv6noPrivate monitoring addresses; IPv4 otherwise uses public_ipv4
log_ipv4 / log_ipv6noPrivate log-source address metadata
releasenoPer-node immutable override of the global release
extra_envnoExplicit per-node Compose overrides; always preserved in the generated .env.prod, including variables that have Compose defaults
enablednoExclude disabled nodes from rendering and targets
drainingnoKeep 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:

json
{
  "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:

dotenv
EDGE_ID=11111111-2222-3333-4444-555555555555
EDGE_BOOTSTRAP_TOKEN=the-one-time-token

Paste them into the prepared host's mode-0600 .env.prod, then start the edge profile explicitly:

bash
sudo docker compose --env-file .env.prod --profile edge up -d

Alternatively, 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:

json
"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 ​

json
{"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 ​

json
{"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 ​

json
{
  "mode": "all-stateful",
  "repository": "s3:s3.example.com/cdnfoundry-production",
  "region": "us-east-1"
}

Modes are disabled, control, and all-stateful.

Exit codes ​

CodeMeaning
0Success
2Command-line usage error
3Invalid topology, config, or failed doctor check
4Missing, locked, or inconsistent fleet state
5Compose, PKI, file-copy, or bundle rendering failure
130Operator interrupted the command

Generated bundle contract ​

Every rendered node directory includes:

text
.env.prod
generated Compose manifest
README.md
validate.sh
start.sh
pki/
secrets/
generated/              # when required
referenced docker/...   # only runtime files used by selected services

DNS 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 0600 at render and transfer time. During control activation, generated start.sh changes only pki/edge-identity-ca.key to owner root, numeric group 82, mode 0640, 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.conf to root:82, mode 0640. PowerDNS receives supplementary group 82 while 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 environment mappings remain explicit per-service allowlists. Replacing them with a shared env_file entry 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.

CDNFoundry documentation