Provisions four DigitalOcean droplets and one cloud firewall.
| Resource | Spec |
|---|---|
base-staging |
s-8vcpu-16gb-amd, nyc1, Ubuntu 24.04 |
base-staging-validator |
same |
base-prod |
same |
base-prod-validator |
same |
base-hosts firewall |
TCP 22 from operator /32 only; TCP 80/443/8080 + ICMP world; outbound open |
Region note: Plan text asked for
nyc3. On this DO account thes-8vcpu-16gb-amdslug is not offered innyc3(API regions list + create returns 422).nyc1is the New York metro region where the size is available. Overrideregioninterraform.tfvarsif capacity returns to nyc3.
Size note: the plan named
s-8vcpu-16gb($96). That slug is no longer in the DO catalog for this account.s-8vcpu-16gb-amdis the current 8 vCPU / 16 GB Basic AMD size (~$112/mo). Memory/vCPU match the approved capacity.
export DIGITALOCEAN_TOKEN=... # never commit; doctl config is fine sourcecd deploy/terraform
cp terraform.tfvars.example terraform.tfvars
# edit ssh_key_ids + operator_ipv4_cidrs
terraform init
terraform validate
terraform plan -out=tfplan
terraform apply tfplanState files are gitignored. Prefer a remote backend before multi-operator use.
cloud-init.yaml.tftpl installs Docker Engine + Compose plugin + age only.
DO metadata is world-readable on the droplet path — no tokens, wallets, or
age private keys in user-data.
Secrets never travel through Terraform or cloud-init.
- Generate / hold identity off-box (operator laptop or HSM path):
- Private:
/root/.base-secrets/age-identity.txtmode600(example path) - Public:
age-keygen -y age-identity.txt→ recipient string
- Private:
- Encrypt env files on the operator machine:
./deploy/scripts/age-encrypt-env.sh \ --recipient age1... \ --src-dir deploy/env \ --out-dir /tmp/base-env-age
- Deliver identity out of band once (USB, 1Password,
scpover the already-firewalled SSH from the operator IP — not via cloud-init):ssh root@<droplet-ip> 'mkdir -p /etc/base && chmod 700 /etc/base' scp /path/to/age-identity.txt root@<droplet-ip>:/etc/base/age-identity.txt ssh root@<droplet-ip> 'chmod 600 /etc/base/age-identity.txt'
- Push ciphertext and materialize on the box:
./deploy/scripts/age-push-env.sh --host root@<droplet-ip> --age-dir /tmp/base-env-age # on droplet: export AGE_IDENTITY=/etc/base/age-identity.txt /opt/base/deploy/scripts/materialize-env.sh
Ciphertext (*.env.age) may live in a private ops store; plaintext *.env
stays mode 0600 on the droplet only. Git already ignores *.age and
deploy/env/*.env.
terraform destroyDoes not touch unrelated DO resources (k8s workers, other firewalls).