Files
warmbly/deploy/split-cloud
..

Split deployment

The control plane on a container host, the fleet on machines you own, and the database, root key and object store in a cloud region next to the control plane. Three providers, one instance.

container host          your machines           cloud region
──────────────          ─────────────           ────────────
backend                 worker                  Postgres
consumer                worker                  KMS key
realtime                nats + redis            S3 bucket
tracking                                        SES
forms
web / admin

The full walkthrough, with costs and the order to build it in, is Split deployment.

What is here

Path What it is
control-plane.env.example Every setting the container host needs, annotated
bus/docker-compose.yml NATS JetStream and Redis, both over TLS
bus/nats.conf The bus config the compose file mounts
bus/certbot-deploy-hook.sh Publishes renewed certificates where the containers can read them
node/docker-compose.yml A worker run by hand, for the version-controlled path
node/worker.env.example What a worker needs, and what it deliberately does not
../../scripts/aws-bootstrap.sh The KMS key, bucket, database and IAM policies

Two things that are not optional

Redis over TLS. The cache holds each organization's decrypted data key for the life of its entry. A plaintext connection across the internet publishes key material, and a password does not change that.

S3 rather than filesystem blobs. A worker reads the message body the backend wrote. On another machine it has neither the disk nor the permissions, so sends fail at the last step with everything else looking healthy.

What a node does not get

A joining node is handed KMS_PROVIDER=brokered and BLOB_PROVIDER=brokered rather than the control plane's own AWS providers, so no machine in the fleet carries a cloud credential. It opens sealed keys and signs blob operations through the internal API, with the instance-scoped token it already holds. Blob bytes still travel directly between the node and the object store.

A worker also never receives PRIMARY_DB. A consumer does, because it is control plane and updates relational state itself.