Configuration
Everything the installer does comes from one JSON file, conventionally cc.config.json. This is the reference for every section of it.
The configuration file
A cluster is described by one JSON file — nodes, networking, storage, packages, registries. Every installer command takes its path:
You don't have to write it by hand. community-cloud with no arguments walks through building one, and can edit an existing file:
The file holds secrets — the cluster token, registry passwords, a Nebula API key — so keep it out of version control. The upstream repository ignores *.config.json for this reason. What the installer records in the cluster has those stripped out.
Treat the machine holding it as the most privileged machine you have. cc.config.json is the cluster token, the registry credentials, and the Nebula API key in one place, usually next to an SSH key with root on every node. A dedicated administrative host beats a laptop.
A small complete example
{
"config": { "adminEmail": "you@example.com" },
"k3s": { "token": "a-long-shared-secret" },
"values": {
"domain": { "main": "example.com" }
},
"network": {
"congestionControl": "bbr",
"gateway": {
"mode": "port-forward",
"addresses": ["192.168.1.240-192.168.1.250"]
}
},
"packages": { "cert-manager": true, "cloudnative-pg": true },
"nodes": [
{
"name": "server-1",
"address": "server-1.local",
"username": "kyle",
"type": "server",
"gateway": true,
"roles": ["worker", "storage-local"],
"region": "eur",
"zone": "eur-de-1"
}
]
}
nodes
The only required section. Each entry is one machine.
| Key | Meaning |
|---|---|
name |
What to call it. Also how it registers in the cluster, when it can be a DNS label. |
address |
How to reach it over SSH. Often an alias from ~/.ssh/config rather than a name DNS knows. |
type |
server or agent. Exactly one node should be a server. |
username |
The SSH user. Needs passwordless sudo. |
keyFile |
A private key path. Otherwise the usual ~/.ssh keys and your agent are tried. |
port |
SSH port, when it isn't 22. |
apiAddress |
Where other nodes reach this one's Kubernetes API, when that differs from address. A bastion or port forward gets you to a node without being a name the cluster can use. |
nodeName |
What to register as in the cluster, when name and address can't be DNS labels. "Mamoru BKK" and 10.0.0.5 both can't. |
gateway |
true if the outside world can reach this node. See network.gateway. |
roles |
What it's for: worker, worker-gpu, storage-local, storage-distributed, storage, gateway, lighthouse. Applied as node-role.kubernetes.io/<role> labels. |
labels |
Anything else worth labelling it with, as key/value pairs. |
region, zone |
Where it physically is. Kubernetes spreads workloads across zones. Region codes are fixed — see installer/src/topology/regions.ts. |
storage |
Per-node storage settings, overriding the cluster-wide storage section. |
nebula |
Per-node mesh settings: role, listenPort, staticAddresses, isLighthouse, isRelay. |
Some roles are worked out rather than written down: a node with a GPU gets worker-gpu, and one with "gateway": true gets gateway.
k3s
| Key | Default | Meaning |
|---|---|---|
token |
— | The cluster token. The server and every agent need the same one. Required. |
version / channel |
current stable | Pin the K3s release. |
clusterCidr |
10.42.0.0/16 |
The pod network. Should agree with the Cilium values. |
disable |
— | Packaged components to leave out, e.g. metrics-server. |
extraServerArgs, extraAgentArgs |
— | Anything else to pass through. |
kubeconfig |
/etc/rancher/k3s/k3s.yaml |
Where kubectl should look, for a cluster this didn't build. |
traefik, servicelb and local-storage are always disabled, whatever disable says. Cilium's Envoy serves the Gateway API, Cilium's own IP management hands out load balancer addresses, and TopoLVM provisions local volumes. K3s's local-path provisioner also claims to be the default storage class, and Kubernetes allows only one.
network
Kernel networking settings sit at the top of this section; everything else is a subsection.
| Key | Default | Meaning |
|---|---|---|
congestionControl |
bbr |
TCP congestion control. BBR holds throughput up over links that lose the odd packet, which CUBIC reads as congestion. |
qdisc |
fq |
The default queueing discipline. BBR wants a fair-queueing one underneath it. |
congestionModule |
tcp_<algorithm> |
The kernel module, when it isn't named after the algorithm. |
network.cilium
| Key | Default | Meaning |
|---|---|---|
version |
1.20.1 |
The Cilium release. |
cliVersion |
v0.20.0 |
The Cilium CLI. "stable" takes whatever is current instead. |
gatewayApiVersion |
v1.6.1 |
The Gateway API release. Pinned deliberately: a mismatched pair fails quietly. |
kubeProxyReplacement |
true |
Whether Cilium takes over from kube-proxy. K3s reads this too. |
valuesFile |
the embedded one | Override the Cilium values. |
network.gateway
How the outside world reaches the cluster. The gateway bundle turns this into a Cilium address pool, and on a home network an ARP announcement policy as well.
| Key | Meaning |
|---|---|
mode |
floating — the address is already on an interface of a gateway node, routed there by a provider. port-forward — a router maps ports to a LAN address nothing holds, and Cilium answers ARP for it. |
addresses |
What a LoadBalancer Service may be given: "203.0.113.10", "10.0.0.0/29", or "192.168.1.240-192.168.1.250". |
interfaces |
port-forward only. Which interfaces to answer ARP on, as regular expressions. Defaults to every local interface. |
name |
What to call the pool and policy. Defaults to community-cloud-gateway. |
network.externalIPs
Addresses a particular node holds, as node name to address. This is the floating case written per node, and the node must be a gateway.
"externalIPs": { "Brahma": "203.0.113.10" }
network.nebula
A Defined Networking mesh, for nodes that can't reach each other directly.
| Key | Meaning |
|---|---|
apiKey |
A Defined Networking API key. Falls back to the DEFINED_API_KEY environment variable. |
network / networkID |
Which network, by name or by ID. |
defaultRole |
The role nodes enrol under. |
roles |
Roles to create: name, description, and firewallRules of protocol (ANY/TCP/UDP/ICMP), allowedRole or allowedTags, and portRange. |
storage
Which disks TopoLVM may carve volumes out of. Can also be set per node, which is usually where it belongs — nodes differ in their disks more than in anything else.
| Key | Meaning |
|---|---|
disks |
Consider only these, by path. This narrows what's considered; it never overrides a safety check. |
diskClasses |
Put a disk in a class other than the one its hardware suggests: {"/dev/sdb": "hdd"}. |
minimumSizeGb |
Ignore disks smaller than this. Defaults to 1. |
Disks are sorted by what they are — NVMe to ssd, other solid state to ssd-sata, spinning to hdd. See Storage for how volume groups are built and which safety checks apply.
packages
Which Helm charts to install. true is shorthand for enabled.
"packages": {
"cert-manager": true,
"cloudnative-pg": true,
"topolvm": { "enabled": true, "version": "15.5.2" }
}
In the catalogue: cert-manager, cloudnative-pg, topolvm, headlamp, tailscale-operator. Dependencies are worked out and installed first — TopoLVM needs cert-manager, so enabling it enables that too.
A package not in the catalogue needs repo and chart. Any package takes version, namespace, releaseName, valuesFile (relative to the working directory) and requires.
Some packages bring credentials with them, such as the database passwords applications connect with. Those aren't configured here; they're generated during the install. See Generated credentials.
Applications live in plugins rather than in the catalogue — Authentik is the worked example. See plugins below.
plugins
Which plugins to load from ~/.community-cloud/plugins/. Being in that directory isn't enough: a plugin loads only when the configuration names it, and only when its fingerprint matches.
"plugins": {
"authentik": {
"enabled": true,
"sha256": "481ec17bce6555d9ea01cb23ec1bdf6290bb0b7644be562a0fdc298b8bfbd4c2"
}
}
Enable one without a sha256 and the installer prints everything it would add — the charts, the manifests, the credentials, and the actual shell of every command — then refuses to run and gives you the line to paste. See Plugins.
registries
Credentials for private registries, keyed by hostname. These end up in /etc/rancher/k3s/registries.yaml, owned by root and readable by nobody else.
"registries": {
"registry.gitlab.com": {
"auth": { "username": "deploy-token", "password": "..." }
}
}
auth takes either username and password, or one of the encoded forms: auth (base64 of username:password) or identityToken. tls takes caFile, certFile, keyFile, insecureSkipVerify. endpoint and rewrite are for mirroring.
values and config
What the manifests render against. Anything in values is available as .Values.<path>. The top level of the configuration is too, so config.adminEmail reaches a manifest as .Values.adminEmail.
The manifests currently reference adminEmail, domain.main, domain.auth, domain.agent, domain.crm, domain.mattermost and authentik.secretKey.
A few values are derived rather than written:
| Value | Derived from |
|---|---|
k8sApiServer, k8sApiPort |
The control-plane node |
l2Announcements |
The gateway mode |
networkDevices |
The shared interface list |
pipelines
Named sequences of bundles, for run-pipeline:
"pipelines": {
"new-node": ["base", "network", "gpu", "k3s", "nodeLabels"]
}
helm
version pins the Helm release. That's all it takes.
Drift
Once a cluster exists, the cluster bundle records its configuration in the cluster itself as a ConfigMap in kube-system called community-cloud, with the secrets stripped out. Finding it is how anything can tell this is a Community Cloud cluster, and doctor compares it against your file to report anywhere they've drifted apart.