3.3 Deployment topologies and sizing
SpatiumDDI runs on one VM or across a multi-node cluster. Adding DNS and DHCP servers never needs a reinstall, and on the OS appliance neither does growing the control plane into a cluster. The decisions that are hard to undo come first: which shape, which install method, and how big. This module walks through the seven reference topologies from the docs, the three ways to install, the sizing numbers the project has measured, and the ports each piece needs.
What you’ll learn
Section titled “What you’ll learn”- Match an environment to one of the seven reference topologies
- Choose between Docker Compose, the Helm chart and the OS appliance
- Size a control plane and agent nodes from the measured numbers, and know where those numbers stop
- List the ports each component needs and apply the fleet firewall basics
What scales, and how
Section titled “What scales, and how”apiandfrontendare stateless: run as many as you like behind a load balancer.workerscales out too, sharded by queue. Thebundlesqueue renders the DNS agents’ config.beatis always exactly one.- PostgreSQL goes HA with CloudNativePG, on Kubernetes and on the appliance. Compose HA isn’t supported in 1.0 (see topology 4).
- Redis goes HA with Sentinel.
- DNS and DHCP agents run one per server, and you add servers as you need them.
Because agents serve from their cache, a control-plane outage doesn’t take DNS or DHCP down with it. That’s what makes the separated topologies below worth the extra machines.
The seven reference topologies
Section titled “The seven reference topologies”1. Single VM
Section titled “1. Single VM”Everything runs in one Docker Compose stack: api, worker, beat, frontend, migrate, PostgreSQL, Redis, BIND9 and Kea. It’s the simplest setup and a single backup unit. But every service shares the host (a DHCP storm can slow the API down) and there’s no HA. Use it for labs, homelabs and trials; your Academy lab is a variant of it.
2. Control plane plus separate DNS and DHCP servers
Section titled “2. Control plane plus separate DNS and DHCP servers”This is the shape most production deployments reach for first. The control plane gets its own VM, and DNS and DHCP agents run on dedicated machines at the network edge, long-polling the API. A control-plane outage doesn’t stop DNS or DHCP, the edge servers get their own firewall rules, and you size each box for its own workload.
Plain Docker agents use the standalone compose files (docker-compose.agent-dns-bind9.yml, docker-compose.agent-dhcp.yml) with the bootstrap key. They verify the control plane’s TLS certificate, so with a private CA or a self-signed certificate, mount the CA and set TLS_CA_PATH (#1220). OS appliances installed as an Additional node pair with an 8-digit code instead.
3. DNS and DHCP HA pairs
Section titled “3. DNS and DHCP HA pairs”Two or more DNS servers sit in one group, and records render to all of them. A Kea HA pair (hot-standby or load-balancing, DHCPv4 only) sits in one DHCP group. Each host is its own VM. Use this when DNS or DHCP downtime isn’t acceptable; module 3.4 covers the mechanics.
4. HA control plane with Patroni and Redis Sentinel
Section titled “4. HA control plane with Patroni and Redis Sentinel”Two or more API hosts sit behind a load balancer, with a 3-node Patroni cluster for PostgreSQL, a 3-node Redis Sentinel set, and a single beat. If you installed the OS appliance, use topology 7 instead.
5. Hybrid cloud
Section titled “5. Hybrid cloud”The control plane runs in your cloud account, and DNS and DHCP agents stay on-prem: DNS should answer close to the clients, and DHCP has to be on the same broadcast domain as its clients. Agents long-poll over HTTPS and can lose the connection for hours while they serve from cache; what they collect meanwhile (query logs, lease events, metrics) is spooled to disk and replayed when the link returns.
Expect 80 to 150 ms per UI or API round trip across regions, terminate TLS at the cloud load balancer (ACME or your own certificate), and let audit, SMTP and webhook traffic out of the cloud side.
6. Kubernetes
Section titled “6. Kubernetes”The umbrella Helm chart runs every component: an api Deployment that its autoscaler runs at 2 to 10 replicas on CPU, fixed-replica workers and frontend (2 each by default), one beat pod, a migrate Job per Helm revision, CloudNativePG, Redis Sentinel and optional DNS and DHCP agent StatefulSets. Pick it when Kubernetes is already your platform.
7. OS appliance, multi-node HA
Section titled “7. OS appliance, multi-node HA”Install the first appliance as the First node, install two more as Additional nodes, and promote them from the Fleet UI into a 3-node cluster (later 5 or 7). You get embedded etcd, CloudNativePG, Redis Sentinel and a MetalLB VIP. The docs call it the answer to “I want HA without becoming a Postgres + Kubernetes operator”.
Two things to know before you rely on it. CloudNativePG replicates asynchronously, so a failover can lose the last few commits. And never grow a cluster by installing a second First node: that creates a separate SpatiumDDI, not a member.
Moving between topologies is additive. From 1 to 2 to 3, new machines join the same control plane with the agent bootstrap key and the database doesn’t move. On the appliance you grow from 1 to 3 to 5 nodes by promoting, with no reinstall.
Answer a few questions about the install. The result is a starting point: adding DNS and DHCP servers never needs a reinstall, and on the OS appliance neither does growing the control plane into a cluster. Moving a Docker Compose install to an HA control plane does mean a new install, so weigh that one up front.
Deployment methods
Section titled “Deployment methods”The README’s use case: dev and small single-host production.
- Needs Docker Engine 25+ and Compose v2.20+, and at least 2 vCPU and 4 GiB of RAM (4 vCPU and 8 GiB recommended).
- Before first start, change
POSTGRES_PASSWORDand setSECRET_KEYin.env(openssl rand -hex 32). The api refuses to start on the placeholder or on a key under 32 characters (#1222). - Pin the release in
.env(SPATIUMDDI_VERSION=1.0.0-beta.3). Betas are GitHub pre-releases, so thelatestimages don’t move to them. - The API port is published on
127.0.0.1only (API_BIND); browsers and remote agents use the web port. - TLS is yours to add: a reverse proxy on the host, Caddy, or TLS inside the frontend’s nginx. Behind your own proxy, set the External URL (Settings → Application → Branding & URL) and
FRONTEND_TRUSTED_PROXY_CIDRS(see the reverse-proxy guide in Go deeper). - Compose HA isn’t supported in 1.0. For HA, use the appliance (topology 7) or Kubernetes with CloudNativePG.
- The appliance fleet firewall doesn’t apply; firewall the host with your own tooling.
The README’s use case: multi-node production.
- Needs Kubernetes 1.31+, Helm 3.8+ and a
ReadWriteOnceStorageClass. Pass--version 1.0.0-beta.3: Helm skips pre-releases when you don’t. - PostgreSQL and Redis run in-chart, single-node by default. HA is one setting each (
postgresql.kind: cnpg,redis.kind: sentinel), but the CloudNativePG operator must be installed first. - DNS and DHCP agents are off by default, and their Services default to
ClusterIP, reachable in-cluster only (#3229). To answer clients on port 53, expose them through MetalLB or another load balancer. On k3s, disable the bundled servicelb first: it takes the node’s own port 53 and the node stops resolving. DHCPv4 needshostNetwork: trueor a relay. - The chart generates the
SECRET_KEY(the at-rest encryption key) per release name. Pin it withauth.existingSecret, or a reinstall under a new release name gets a new key and can’t decrypt the secrets already stored. - Upgrading a release from before 1.0.0-beta.3: if it’s older than 2026.10.02-1, upgrade to that bridge release first (2.6). Then run
helm upgradeonce with--reset-then-reuse-valuesto take the new api and worker resource defaults (--reuse-valueskeeps the old ones), and giveservice: { type: LoadBalancer }to every agent server that must keep its external IP.
The README’s use case: the simplest install, including air-gapped sites, managed from a dedicated /appliance hub. It’s still marked beta.
- A Debian 13 image with embedded k3s: HTTPS and the full stack in 2 to 5 minutes, and no
kubectlordockercommands to learn. - The installer’s What is this node? screen offers First node (a new control plane and the k3s seed) and Additional node, the default: it joins an existing install with an 8-digit pairing code, runs the DNS and DHCP roles you assign, and can be promoted into the control plane later.
- On a single box, DNS and DHCP are off after install. Turn them on for the node in Appliance → Fleet.
- Atomic A/B slot upgrades, air-gapped boot with baked images, and the declarative fleet firewall. The arm64 image is UEFI-only, and the installer can mirror the OS across two disks (RAID1).
- The cost: k3s adds about 70 MB to the image and about 150 MB of RAM, and the installer refuses a disk under 32 GiB.
Sizing: what the project measured
Section titled “Sizing: what the project measured”The sizing guide gives a measured Minimum and a Recommended size per shape, from appliances, Compose and Kubernetes installs measured in October 2026:
| Shape | Minimum | Recommended |
|---|---|---|
| Docker Compose, one host | 2 vCPU, 4 GiB | 4 vCPU, 8 GiB |
| Kubernetes, the umbrella chart | one node: 4 vCPU, 6 GiB | 4 vCPU, 8 GiB per node |
| Appliance, one node (control plane, with or without DNS and DHCP) | 4 vCPU, 6 GiB, 40 GiB disk | 4 vCPU, 8 GiB, 40 GiB SSD |
| Appliance, three-node control plane | first node 4 vCPU and 8 GiB, two more 2 vCPU and 6 GiB, 40 GiB disks | every node 4 vCPU, 8 GiB, 48 GiB SSD |
| Appliance role (a DNS and/or DHCP agent box) | 3 GiB (its pods don’t schedule below that) | 2 vCPU, 4 GiB, 32 GiB SSD; not yet measured |
Topology 1 on Compose has its own figure in the topologies guide: 4 vCPU, 8 GB and a 50 GB SSD comfortably handle about 5,000 IPs, 50 subnets and 50 zones. What the measurements show:
- Memory is mostly the stack, not the load. One appliance node used 3.3 GiB at rest and 4.1 GiB in the busiest suite, and the same on 8 GiB. The 6 GiB Minimum is the measured floor, not a comfortable size, and the installer warns below 8 GiB.
- Disk is set by upgrades. Each upgrade imports a full set of images onto
/var, and the appliance keeps the previous release’s for rollback. On 40 GiB disks a slot upgrade followed by a rolling upgrade reached the default-onappliance_disk_usagewarning (80%; critical at 87%), hence 48 GiB for a cluster. Give every control-plane node 64 GiB or more for about a million devices. - Large DNS groups need memory. Plan for more from 250,000 records up. Each DNS server’s container (agent and daemon under one limit, 2Gi by default) fits a million records only while its resolver cache stays small, and wants 3Gi if it also recurses.
- SSD is strongly preferred for the etcd and PostgreSQL write path.
An older single-node test, from September 2026, loaded one DNS group of 250,000 A records (plus 250,000 PTRs) and 35,000 DHCP devices (20,000 active). 8 GiB with 4 vCPU passed with headroom, 6 GiB with 3 vCPU was the smallest size that passed every time, 4 GiB wedged, and 2 vCPU was CPU-bound.
In that test 500,000 records in one group didn’t fit and 1,000,000 didn’t converge, because the api built each server’s bundle itself. The worker has rendered DNS bundles since 2026.10.02-1 (#1111), so treat those two figures as dated, and keep splitting very large namespaces across groups until you’ve tested your own.
One more finding matters for HA designs: control-plane HA is not serving HA. In the 3-node drill, DNS and DHCP roles sat on the seed node only, so while it was down the survivors answered about 5% of queries. Assign DNS and DHCP roles to every node you expect to keep serving.
The guide is open about its gaps: Compose and Kubernetes were measured only at rest and under light load, nobody has yet measured a DNS-only or DHCP-only appliance or one node under sustained realistic load, and no run lasted longer than 3.3 days.
The guide also shows how to measure your own install: Appliance → Cluster → Overview on the appliance, docker stats on Compose, kubectl top on Kubernetes. Size from your largest used-memory sample times 1.5, plus room to grow.
Ports and protocols
Section titled “Ports and protocols”| Traffic | Port | Protocol | Notes |
|---|---|---|---|
| Web UI (Compose) | 8077 (HTTP_PORT) |
HTTP | Operators to the frontend; the API and its docs are under /api on the same port |
| Web UI with TLS, and the appliance | 443 (80 also open on the appliance) | HTTPS | Operators to the frontend |
| API direct | 8000 (API_PORT) |
HTTP | Compose publishes it on 127.0.0.1 only (API_BIND) |
| PostgreSQL / Redis | 5432 / 6379 | TCP | Internal only, never exposed |
| Agents to the control plane | your control-plane URL | HTTPS | Agents only call out, and they verify its certificate |
| DNS service | 53 | UDP, TCP | Standalone Compose agent files map 1053 on the host by default |
| DHCP service | 67 (68 on the client side); 547 for DHCPv6 | UDP | Standalone Compose agent files map 6767 by default |
| Kea HA peer to peer | 8000 | TCP | Between a group’s Kea servers only; HTTPS with client certificates once the group’s credential is active |
| DoT / DoH / DoQ | 853 / 443 / 853 | TCP / TCP / UDP | Per group, off by default; use 8443 for DoH on the appliance |
| Windows servers | 5986 (HTTPS) or 5985, and 53 | TCP; UDP and TCP | Control plane to Windows: WinRM, RFC 2136 and AXFR |
| FortiGate | 443 | HTTPS | Control plane to FortiOS REST |
| Appliance cluster | 6443, 2379/2380, 10250, 7946, 19443 | TCP (7946 also UDP) | Control-plane peers only; 19443 is the frr-k8s webhook MetalLB uses for BGP |
| SSH to an appliance | 22 | TCP | Open by default; only Enforce source restriction can scope it |
The PowerDNS and Technitium APIs (127.0.0.1:8081, 127.0.0.1:5380) listen on loopback inside their containers and are never exposed.
Fleet firewall basics
Section titled “Fleet firewall basics”The appliance ships a declarative firewall you author on the control plane and the supervisor applies to each node. Compose and generic Kubernetes installs firewall their hosts with their own tools.
- Off by default, behind two gates: the
appliance.firewallfeature module, and thefirewall_enabledenforcement switch. - Layered policy: a fleet baseline, then per-role overlays (DNS engines, DHCP, control plane), then per-appliance overrides. SSH on 22, ICMP and loopback form a floor no rule can remove, and no rule may drop port 22. Only the separate SSH source restriction (Enforce source restriction,
ssh_lockdown, off by default) can scope SSH (#1009). - Per role: DNS-only and DHCP-only nodes open only their service ports (53; 67/68 and 547 for DHCP) plus the floor, and never etcd or kubelet. Control-plane members open etcd, kubelet, MetalLB’s 7946 and the 19443 webhook to their cluster peers only, never LAN-wide. Kea HA members open the HA port to their partners only.
- Enforcement is gated: it refuses to turn on until every node reports hardened. Stage and review changes in Preview changes first.
- Web UI access: a card on the Firewall tab limits ports 80/443 (and the VIP) to source CIDRs you list. It rejects a list that doesn’t cover your own IP unless you override it, and a Web UI or SSH change that would leave no remote way in at all needs an explicit console-only acknowledgement (#1013).
Knowledge check
Section titled “Knowledge check”Go deeper
Section titled “Go deeper”- Production deployment topologies: all seven shapes, the picker table and the fleet firewall per topology
- Sizing: measured Minimum and Recommended per shape, what isn’t measured yet, and how to measure your own install
- Docker Compose deployment: ports, environment variables, TLS options and distributed agents
- Running behind a reverse proxy: the External URL, trusted proxies, headers and streaming
- Kubernetes deployment: the chart, autoscaling, HA settings and exposing agents
- Install the OS appliance: the installer screens, First node and Additional node, and growing to HA
- OS appliance: control-plane HA, the measured floors and the fleet firewall
- Fleet firewall design: the policy model in full