- driver replicas are per-instance nodes with their own RabbitMQ command queues (group=dc3.driver.client, key service.node), not workers over shared queues; FAQ now also states Compose/Swarm keep protocol drivers at 1 replica (shared driver_data / SQLite outbox) - fail-fast gate covers AUTH_HMAC_SECRET only; DC3_SECURITY_KEY is non-empty-checked, so a weak default still boots - 6271 is intended as the device UDP channel but every publish site omits /udp / protocol: UDP and actually publishes TCP - say so - k8s scaling note: scaled driver pods are independent nodes, not one shared worker pool - swarm runbook: pushing dependency images is a multi-node requirement, a local build is fine on a single node - USAGE.md: NODE_ENV bullet narrowed to weak AUTH_HMAC_SECRET rejected
16 KiB
IoT DC3 Deployment Guide
This document covers every deployment mode shipped with IoT DC3, from a single-host Compose stack to Kubernetes and Helm, and the production hardening checklist. It is the companion to the Quickstart and the environment variable reference on docs.dc3.site.
1. Deployment modes at a glance
| Mode | Files | Runtime | Use for | Scaling |
|---|---|---|---|---|
| Single host | dc3/docker-compose-db.yml + dc3/docker-compose.yml |
Docker/Podman Compose | evaluation, demo, small production | none (singleton) |
| Compose scale | dc3/docker-compose-db.yml + dc3/docker-compose-scale.yml |
Docker Compose v2 | single-node production with replicated services | docker compose up --scale <svc>=N |
| Docker Swarm | dc3/docker-compose-swarm.yml |
Docker Swarm mode | multi-node swarm cluster | docker service scale dc3_<svc>=N |
| Kubernetes | dc3/deploy/k8s/ (kustomize) |
any k8s cluster | production Kubernetes | kubectl scale / HPA |
| Helm | dc3/deploy/helm/dc3/ |
Kubernetes | GitOps / repeatable installs | values + HPA |
All modes run the same images with the same environment variables, so a service topology tuned on Compose behaves identically elsewhere. The runtime differences are only about where replicas live and how traffic reaches them.
Service topology and ports
| Service | Image | HTTP | gRPC | Notes |
|---|---|---|---|---|
| web | dc3-web |
80/443 (in container) | - | nginx; proxies /api/ to the gateway |
| gateway | dc3-gateway |
8000 | - | single HTTP entry point for all centers |
| auth | dc3-center-auth |
8300 | 9300 | tenants, users, RBAC |
| manager | dc3-center-manager |
8400 | 9400 | drivers, devices, points, profiles |
| data | dc3-center-data |
8500 | 9500 | point values, commands, dashboards |
| agentic | dc3-center-agentic |
8600 | - | AI agentic center (LLM calls) |
dc3-driver-* |
one image per protocol | - | - | protocol adapters |
| postgres | dc3-postgres |
- | - | PostgreSQL + AGE/TimescaleDB/pgvector, seeded |
| rabbitmq | dc3-rabbitmq |
- | - | AMQP + embedded MQTT broker, TLS-capable |
Ingress discipline: only
web(80/443) andlistening-virtual(device TCP 6270) are ever published to the outside. Every backend port stays on the internal network. Do not map center or database ports to the host in production. Note on 6271: it is intended as the device UDP channel, but every publish site today (Compose, scale, swarm, the k8s Service) omits/udp/protocol: UDP, so it currently publishes as TCP - add the protocol declaration before relying on UDP.
2. Image availability
The release CI (docker-ci.yml) builds and publishes app images (web, gateway, centers, drivers) to Docker Hub
(pnoker/*) and Aliyun (registry.cn-beijing.aliyuncs.com/dc3/*) for every v* tag. The dependency images
dc3-postgres and dc3-rabbitmq are built locally by
docker-compose-db.yml and are not published:
# build + push them once for swarm/k8s/helm (Compose builds them automatically)
DC3_IMAGE_REGISTRY=my.registry/dc3 ./dc3/deploy/k8s/scripts/push-images.sh
Tag layout differs per image family: the backend images (gateway, centers, drivers) are published as latest, the
series tag (2026.6), and the full version (2026.6.0), while
dc3-web is published only as latest and full release versions (no series tag). The deployment configs already
account for this - web pins latest with a documented override (services.web.tag in Helm, the images: block in
kustomize); pin a full release version for dc3-web when you need reproducible rollouts.
3. Mode 0 - single host (Compose)
make up-db # PostgreSQL + RabbitMQ (docker-compose-db.yml)
make up STACK=app # app stack: web, gateway, centers, drivers (docker-compose.yml)
make logs # follow container logs
make down # stop, keep volumes
make reset CONFIRM_RESET_VOLUMES=true # down + delete volumes (destructive)
Use REGISTRY=cn for the Aliyun mirror. All values come from .env (see the environment variable reference on
docs.dc3.site).
4. Mode 1 - Compose with horizontal scaling
dc3/docker-compose-scale.yml is the app topology rebuilt for replicas: no
container_name/hostname pins, no per-replica host ports, and deploy.resources
limits enforced by Compose v2. It intentionally keeps PostgreSQL/RabbitMQ in the separate db stack (both files share
the dc3net project network).
docker compose -f dc3/docker-compose-db.yml up -d
docker compose -f dc3/docker-compose-scale.yml up -d \
--scale gateway=2 --scale data=2
What can scale, and how traffic is balanced
| Tier | Scalable? | Load balancing |
|---|---|---|
| web | 1 replica | publishes 8080/8443 - put your own LB in front for more |
| gateway | yes | the nginx in dc3-web resolves dc3-gateway to every replica and round-robins (restart web after scaling gateway to refresh addresses) |
| centers | yes (HA) | HTTP routes from the gateway are balanced by Spring Cloud Gateway; center-to-center gRPC uses static DNS targets - a replica restart fails over the channel, but the channel is not request-balanced |
| drivers | 1 replica | every replica mounts the shared driver_data volume and would open the same SQLite outbox file; scale drivers on Kubernetes (per-pod emptyDir) instead |
| listening-virtual | 1 replica | inbound device sockets are pinned to one container |
| postgres / rabbitmq | 1 replica | stateful singletons by design |
Gotchas
- Center-to-center gRPC (
static://scheme) keeps one channel per client. For request-level balancing of center gRPC, run Kubernetes (Service round-robin) or add a client-side LB. HTTP traffic through the gateway is balanced in every mode. depends_onwithcondition: service_healthyis honored by Compose; after scaling, new replicas start against the already-healthy stack.
5. Mode 2 - Docker Swarm
dc3/docker-compose-swarm.yml is a self-contained Swarm stack (dependency images included, overlay network, deploy:
blocks for replicas/updates/restarts/resources).
# 1. one-time: a single-node swarm is enough to start
docker swarm init
# 2. build + push dependency images (not published by CI; a local build is fine on a single node)
DC3_IMAGE_REGISTRY=my.registry/dc3 ./dc3/deploy/k8s/scripts/push-images.sh
# 3. deploy (run from the repo root so .env is interpolated)
docker stack deploy -c dc3/docker-compose-swarm.yml dc3
# 4. operate
docker service ls
docker service scale dc3_gateway=3
docker service logs -f dc3_gateway
docker stack rm dc3
Swarm-specific notes
webpublishes 80/443 withmode: ingress- the swarm ingress network balances replicas automatically (docker service scale dc3_web=2is safe).listening-virtualpublishes 6270/6271 withmode: hostand must stay at 1 replica (connection affinity).- Protocol drivers stay at 1 replica: every replica would open the same SQLite outbox file on the shared
driver_datavolume. Scale drivers on Kubernetes instead (each pod gets its ownemptyDir). - Stateful services are constrained to
node.role == managerso their local volumes stay on one node. On a multi-node swarm change the constraint to a dedicated label (e.g.node.labels.dc3-stateful==true) and place the volumes on shared storage (NFS/Ceph) - thelocaldriver is single-node only. - Swarm ignores
depends_on/build; startup is handled by healthchecks + restart policy, and the centers retry their gRPC channels until dependencies are ready.
6. Mode 3 - Kubernetes (kustomize)
Manifests live in dc3/deploy/k8s/ (see its README.md for the full runbook).
cd iot-dc3
cp dc3/deploy/k8s/secret.env.example dc3/deploy/k8s/secret.env # then edit values
DC3_IMAGE_REGISTRY=my.registry/dc3 ./dc3/deploy/k8s/scripts/push-images.sh
kubectl apply -k dc3/deploy/k8s
kubectl -n dc3 get pods -w
- The kustomize root is the single control point: resource list,
secretGenerator(fromsecret.env), and theimages:block (one place to bump the tag or switch registries). - Stateful services are
StatefulSets with PVCs (postgres 20Gi, rabbitmq 8Gi, agentic 5Gi); the seed SQL runs on the first start of an empty volume, same as Compose. gateway/webship CPU-based HPA (autoscaling/v2) and every stateless service has aPodDisruptionBudget(minAvailable 1) and rolling updates withmaxUnavailable: 0.- Device ingress:
dc3-driver-listening-virtualis exposed as NodePort 30670/30671; switch the Service toLoadBalancerif you prefer. - Ingress routes
/api/todc3-gateway:8000and/todc3-web:80; set the host and TLS (cert-manager example annotation provided) before production. - Scaling semantics: gateway/web/centers/drivers scale - driver pods each get a per-pod
emptyDiroutbox, unlike Mode 1/2 where drivers stay at 1 replica;listening-virtualstays at 1; postgres/rabbitmq are singletons. The k8s Service (kube-proxy) additionally load-balances per TCP connection, so scaled centers also get connection-level distribution for HTTP. Note that a scaled driver's replicas are independent nodes with separate command queues (see the FAQ), not one shared worker pool.
7. Mode 4 - Helm
Chart: dc3/deploy/helm/dc3 (README inside). It renders the same topology as the kustomize manifests, parameterized by
values.yaml / values-production.yaml.
helm upgrade --install dc3 dc3/deploy/helm/dc3 -f dc3/deploy/helm/dc3/values-production.yaml \
--set image.registry=my.registry/dc3 \
--set-string secrets.DC3_SECURITY_KEY=<random> \
--set-string secrets.AUTH_HMAC_SECRET=<random>
helm ls # or: helm status dc3
helm upgrade dc3 dc3/deploy/helm/dc3 --reuse-values --set services.gateway.replicas=4
helm rollback dc3 1
helm uninstall dc3
- Services are generated from the
services:/drivers:maps - enable/disable and tune replicas, resources and probes without touching templates. - Secrets come from the chart-owned
dc3-secretsSecret; setexistingSecretto use your own (GitOps-friendly). listening-virtualgets a NodePort Service fromdrivers.listening-virtual.nodePort.
8. Production hardening checklist
Apply every item before connecting real devices:
- Secrets - replace
DC3_SECURITY_KEY,AUTH_HMAC_SECRET, all database/broker passwords and the LLM API key with strong random values. The fail-fast gate coversAUTH_HMAC_SECRETonly: on thepre/proprofile an empty or still-default value refuses to start;DC3_SECURITY_KEYis checked for non-empty only, so a known weak default still boots. Never commitsecret.env/values-production.yamlwith real values. - TLS - terminate TLS at the edge (web nginx already ships a hardened TLS config; k8s: ingress + cert-manager;
swarm: put a proxy in front of
web). Enable RabbitMQ TLS (RABBITMQ_SSL_ENABLED=true, port 5671) and PostgreSQL TLS for cross-node traffic. - Storage & backups - schedule
pg_dump/pgBackRest with off-site copies; test restore. TimescaleDB chunks grow continuously - plan capacity (FAQ sizing: 8 cores / 16 GB / 100 GB SSD minimum for the full stack). - High availability - postgres: primary/standby (or managed service) + RabbitMQ cluster; stateful volumes on replicated storage for swarm/k8s multi-node.
- Observability - add the optional stack (
docker-compose-optional.ymlin Compose; deploy Prometheus/Grafana + ELK on k8s/swarm) and alert on readiness/liveness. - Network - restrict egress with firewall/NetworkPolicies (centers need the LLM endpoint; everything else is
internal), keep backend ports off the host, enable Pod Security Admission
baselineon k8s. - API surface - Swagger/OpenAPI is disabled on the
proprofile. Release images are built withPROFILE=pro, and at runtime every deployment config selects the profile viaNODE_ENV(testby default; setNODE_ENV=profor the hardened profile - OpenAPI/Swagger UI off, weak secrets rejected). Verify no debug endpoints are reachable.
9. Common questions
- Why does scaling a center not balance every gRPC request? Center-to-center gRPC uses static DNS targets
(
static://host:port) with one channel per client. Replicas provide failover and rollout safety; true gRPC request balancing needs client-side LB or k8s (per-connection round-robin via ClusterIP). HTTP traffic is balanced at every tier (nginx -> Spring Cloud Gateway -> centers). - Can I run drivers at 2 replicas? On Compose/Swarm, no - keep protocol drivers at 1 replica (they share the
driver_datavolume and would open the same SQLite outbox file). On Kubernetes you can scale them (per-podemptyDir), but understand the mechanics: every replica registers as its own node with its own RabbitMQ command queue (group="${dc3.driver.client}", routing keyservice.node, node = per-instance UUID) - replicas are not competing consumers of one shared queue, so scaling adds independent driver instances rather than splitting work per message.listening-virtualalways stays at 1 replica (it owns inbound device sockets). - PostgreSQL/RabbitMQ replicas? Not supported by these configs - they are stateful singletons. For HA run managed
services (or your own cluster) and override the host variables:
POSTGRES_HOST,RABBITMQ_HOST,MQTT_BROKER_HOST(plusCENTER_*_HOST/APP_API_HOSTif you also move the services). Self-managed replacements must provide what the bundled images provide: PostgreSQL with the AGE, TimescaleDB and pgvector extensions (plus the seed SQL on first start), RabbitMQ with the MQTT plugin and thedc3.e.mqttexchange. - Do the k8s/helm configs need the dependency images? Yes; build and push them with
scripts/push-images.sh(orkind loadon single-node clusters).