Skip to content

Part 2 — Provision with Terraform

We’ll use the official runestack/rune/digitalocean module. It does five things in one apply:

  1. Creates the droplet (Ubuntu 24.04) and a firewall.
  2. Cloud-init installs Docker + runed + the rune CLI.
  3. Optionally SSHes back in, runs rune admin bootstrap, and writes the token to your laptop.
  4. Pre-loads docker registry credentials so runed can pull private images on first start.
  5. (Below) we wrap it in a Reserved IP so DNS points to a stable address that survives droplet destroys.

For the non-Terraform alternative, see the DigitalOcean deploy guide. For the full module reference, see the Terraform DigitalOcean guide.

Put these in infra/terraform/do/.

terraform {
required_version = ">= 1.5.0"
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = ">= 2.40, < 3.0"
}
}
}
provider "digitalocean" {
token = var.do_token
}
data "digitalocean_ssh_key" "main" {
name = var.ssh_key_name
}
# Reserved IP — survives droplet destroys. DNS points here once, never changes.
resource "digitalocean_reserved_ip" "rune" {
region = var.region
lifecycle {
prevent_destroy = true
}
}
resource "digitalocean_reserved_ip_assignment" "rune" {
ip_address = digitalocean_reserved_ip.rune.ip_address
droplet_id = module.rune.droplet_id
}
module "rune" {
source = "runestack/rune/digitalocean"
version = "0.0.8"
ssh_key_ids = [data.digitalocean_ssh_key.main.id]
node_role = "edge" # edge: binds :80/:443, runs ACME
acme_email = var.acme_email
environment = var.environment
region = var.region
droplet_size = var.droplet_size
rune_version = var.rune_version
# Pre-load GHCR credentials so first-pull works.
# `from_secret` + `bootstrap` keeps the PAT out of runefile.toml.
docker_registries = var.docker_registries
runed_environment = var.runed_environment
# Auto-bootstrap: SSH in after install, run `rune admin bootstrap`,
# write the token to ./rune-admin.token on your laptop.
bootstrap = true
bootstrap_ssh_private_key = file("~/.ssh/id_ed25519")
bootstrap_token_path = "rune-admin.token"
bootstrap_namespace = var.environment
}
variable "do_token" { type = string sensitive = true }
variable "ssh_key_name" { type = string }
variable "region" { type = string default = "lon1" }
variable "droplet_size" { type = string default = "s-2vcpu-4gb" }
variable "environment" { type = string default = "dev" }
variable "acme_email" { type = string }
variable "rune_version" { type = string default = "v0.0.1-dev.22" }
variable "docker_registries" {
type = any
sensitive = true
default = []
}
variable "runed_environment" {
type = map(string)
sensitive = true
default = {}
}
output "reserved_ip" {
value = digitalocean_reserved_ip.rune.ip_address
}
output "grpc_endpoint" {
value = "${digitalocean_reserved_ip.rune.ip_address}:7863"
}
output "rune_login_command" {
value = "rune login myapp-${var.environment} --server ${digitalocean_reserved_ip.rune.ip_address}:7863 --token-file ${abspath("rune-admin.token")} --default-namespace ${var.environment}"
}

Create infra/terraform/do/terraform.tfvars (this is gitignored from part 1):

do_token = "dop_v1_xxxx..."
ssh_key_name = "my-laptop"
region = "lon1"
droplet_size = "s-2vcpu-4gb"
environment = "dev"
acme_email = "[email protected]"
rune_version = "v0.0.1-dev.22"

3. Pre-load GHCR credentials (skip if you’ll only pull public images)

Section titled “3. Pre-load GHCR credentials (skip if you’ll only pull public images)”

If your CI will push to GitHub Container Registry, runed needs to authenticate to pull. Pass credentials via env vars so they’re never committed:

Terminal window
export TF_VAR_docker_registries='[{
"name":"ghcr",
"registry":"ghcr.io",
"from_secret":"ghcr-credentials",
"bootstrap":true,
"data":{"username":"YOUR_GH_USERNAME","password":"${GHCR_PAT}"}
}]'
export TF_VAR_runed_environment='{"GHCR_PAT":"ghp_..."}'

On first start runed materialises the ghcr-credentials Secret from the bootstrap data, then resolves ${GHCR_PAT} from /etc/rune/runed.env. The PAT never lands in Terraform state as cleartext. The mechanics are spelled out in the GHCR auth guide.

Terminal window
cd infra/terraform/do
terraform init
terraform apply

What happens, in order:

  1. 0:00 — Droplet boots; cloud-init starts installing Docker + runed.
  2. ~2:00runed.service starts, edge mode binds :80/:443, ACME orchestrator registers.
  3. ~2:30 — The module’s null_resource.bootstrap SSHes in, runs rune admin bootstrap, fetches the token to infra/terraform/do/rune-admin.token (mode 0600).
  4. Outputs print:
    reserved_ip = "134.209.xx.xx"
    grpc_endpoint = "134.209.xx.xx:7863"
    rune_login_command = "rune login myapp-dev --server ..."
Terminal window
eval "$(terraform output -raw rune_login_command)"
rune whoami
# Subject: root Policies: [root]

You’re now an authenticated admin on a fresh server. Save the printed command — you’ll need it again to mint the CI service token in part 4.

ResourcePurpose
digitalocean_reserved_ip.runeStable IPv4 for DNS. Has prevent_destroy = true.
module.rune.digitalocean_dropletThe actual box. Recreating it is safe — Reserved IP stays.
module.rune.digitalocean_firewallIngress: 22, 80, 443, 7861, 7863. Tighten with ssh_allowed_cidrs later.
module.rune.null_resource.bootstrapOne-shot SSH + rune admin bootstrap.

Next: Part 3 — Author your runeset → · Back: Part 1 — Project layout