Skip to content

DigitalOcean

From an empty account to a running installation: Terraform builds the cluster, the managed MongoDB, the Spaces, the registry, the load balancer and the DNS records; a pipeline deploys the configuration.

This is the path from an empty DigitalOcean account to a running backd: Terraform builds the infrastructure (a Kubernetes cluster, a managed MongoDB, one or two Spaces, a container registry, a load balancer and the DNS records) and creates the secrets, and a pipeline in your configuration repository builds the image of your configuration and deploys it with the Helm chart. The files are in deploy/terraform/digitalocean and deploy/kubernetes of the repository; the reasoning is in design/kubernetes.md.

What you get, and what it costs

PartDefault$ a month
Kubernetes clusterstandard (free) control plane, 2 nodes of 2 vCPU / 4 GB48
Load balancerone lb-small, for the API and the admin host12
MongoDBmanaged, 1 node of 1 vCPU / 2 GB30.51
Spacesone private Space (250 GiB and 1 TiB of transfer included)5
Container registrybasic (starter is free and works: one repository)5
Totalabout 100

The choices that change the price: database_nodes = 3 (a primary and two standby nodes, three times the database: about $91.53 for the 2 GB size), a bigger database_size, node_size and node_count, a high-availability control plane (ha_control_plane, about $40), a second Space for public files, and registry_tier. Prices are DigitalOcean’s at the time of writing; check their pricing pages.

What you need

  • A DigitalOcean account, an API token with read and write access, and one Spaces key that can create buckets (Spaces keys that Terraform makes are bound to one Space, so this first one is yours: API > Spaces Keys).
  • A domain on DigitalOcean DNS (Networking > Domains), with its name servers pointing there.
  • A second API token for cert-manager with write access to Domains only.
  • Terraform 1.9 or later, kubectl and helm.
  • A repository for your configuration (config/ is CONFIG_DIR, as in Deploying config).

1. The state bucket

The Terraform state holds every generated secret, so it lives in a private Space of its own, made once by bootstrap/:

export DIGITALOCEAN_TOKEN=...  SPACES_ACCESS_KEY_ID=...  SPACES_SECRET_ACCESS_KEY=...
cd deploy/terraform/digitalocean/bootstrap
terraform init
terraform apply -var state_bucket_name=acme-backd-state -var region=fra1
terraform output -raw backend_config > ../backend.hcl
export AWS_ACCESS_KEY_ID=$(terraform output -raw access_key_id)
export AWS_SECRET_ACCESS_KEY=$(terraform output -raw secret_access_key)

2. The installation

cd ..                                   # deploy/terraform/digitalocean
cp terraform.tfvars.example terraform.tfvars   # fill it in: the file explains each variable
export TF_VAR_dns_token=...             # the Domains-only token
terraform init -backend-config=backend.hcl
terraform apply

The database needs no CA file: DigitalOcean’s MongoDB presents a Let’s Encrypt certificate, which the pods verify against the system roots (the check is never skipped). Set database_ca only if you front the database with a CA of your own.

The pods connect to the database over the private network only. The database nevertheless advertises its members under their public names, which the network policy does not allow; Terraform reads the private address of each member from DNS and the chart makes the public names resolve to them inside the pods (see Network policies). With one node this is exact; with three, the public and private names are paired in name order, which has not been checked on a real cluster. Run terraform init after updating: the configuration now also uses the dns provider.

Set acme_staging = true for the first run (certificates browsers do not trust, with far higher limits) and switch it off once everything works. The apply takes ten to fifteen minutes, most of it the cluster and the database.

VariableWhat it changes
installThe name of everything (prefixes, tags, BACKD_DEPLOYMENT_ID)
project_name, project_environment, project_purposeThe DigitalOcean project Terraform creates for the installation (default name: install; environment Production)
node_size, node_count, node_min + node_maxThe workers, or autoscaling between two numbers
database_nodes1, or 3 for a primary and two standby nodes; nothing in between
database_size, database_versionThe size of each database node, and the MongoDB version
database_caOptional CA (PEM) to verify the database with instead of the system roots; leave unset on DigitalOcean
bucketsprivate, or private_and_public (the second Space holds the files of public: true fields)
private_bucket_name, public_bucket_nameThe names of the Spaces (unique per region)
public_cdn, assets_hostA CDN in front of the public Space, optionally under your own name
registry_tierstarter, basic (default) or professional
domain, api_host, admin_hostWhere the installation answers
lb_size, lb_algorithmThe load balancer
control_plane_allowed_cidrs, database_allowed_cidrsExtra restrictions on the Kubernetes API and the database

Terraform creates a DigitalOcean project (project_name) and puts the cluster, the database, the Spaces, the load balancer and the domain in it, so they appear together in the control panel. The domain is moved into it from wherever it was; the network, the registry and the CDN cannot belong to a project and stay outside.

Nothing on the nodes is reachable from the internet: the firewall allows only the private network, the database accepts only the cluster, and the Spaces and the registry are private.

3. The first release

Put the pipeline in your configuration repository (Kubernetes): deploy/kubernetes/pipeline/release.yml as .github/workflows/release.yml, and deploy/values.yaml from values.example.yaml with the values Terraform printed. Set the repository variables and the secret the workflow lists (terraform output registry_endpoint, cluster_name, and a token for the pipeline), then tag:

git tag release-2026-10-30-1 && git push origin release-2026-10-30-1

The workflow checks the configuration, builds and pushes the image, upgrades the chart, and the pods start in verify mode against a database it has just provisioned.

4. The first administrator and the storage keys

Create the first administrator of each realm from inside the cluster, then give the realm its storage keys (they are realm secrets, kept encrypted in MongoDB, not environment variables):

kubectl exec -n backd deploy/backd-admin -- backd-server bootstrap --realm shop --email me@example.com

terraform output -json spaces_private_key      # access_key, secret_key
backd secret set --realm shop --name STORAGE_ACCESS_KEY --url https://admin.example.com
backd secret set --realm shop --name STORAGE_SECRET_KEY --url https://admin.example.com
backd storage check --realm shop --url https://admin.example.com

terraform output -raw realm_storage prints the storage: (and public_storage:) section for the realm’s realm.yaml. The admin host answers only from the addresses you gave in ingress.admin.allowedCIDRs; see Kubernetes for changing them.

Changing the installation

  • From one database node to three: set database_nodes = 3 and apply. DigitalOcean adds the standby nodes to the same cluster, the connection string gains w=majority, and the new URI reaches the pods with the next rollout. The configuration and the data do not change.
  • A bigger cluster: change node_size or node_count and apply.
  • A new configuration: tag the configuration repository. A rollback is the previous tag, or helm rollback.
  • A new backd release: change BACKD_VERSION in the workflow’s variables and tag; read the Upgrading section of Operate first.
  • Destroying it: terraform destroy removes the infrastructure and the project. The domain, which existed before, goes back to your default project with its records. The Spaces are never emptied by Terraform: if they hold files the destroy stops until you empty them yourself, and the state bucket from step 1 is yours to remove.

What DigitalOcean does not allow

The other limits worth knowing: a Spaces key reaches every prefix of its Space (use a Space per installation), and a one-node database has no failover and goes down for its maintenance window.

Edit this page · Last updated 13 Nov 2026