Part 1 — Project layout
Spend five minutes on layout now and the rest of the tutorial slots in cleanly. The shape we’ll use:
myapp/├── apps/│ └── web/ # Your application code│ ├── Dockerfile│ └── ...├── infra/│ ├── terraform/│ │ └── do/ # Terraform root for DigitalOcean│ │ ├── main.tf│ │ ├── provider.tf│ │ ├── variables.tf│ │ ├── outputs.tf│ │ └── terraform.tfvars # gitignored — your real values│ ├── runeset/│ │ ├── runeset.yaml # name + version + namespace│ │ ├── casts/│ │ │ └── web.yaml # one cast per service│ │ ├── values/│ │ │ ├── dev.yaml│ │ │ └── prod.yaml│ │ ├── configs.example.yaml # configmap reference (committed)│ │ └── secrets.example.yaml # secret schema reference (committed)│ └── runefile.toml # optional: rendered server config reference└── .github/ ├── deploy-config.yml # which paths trigger which deploys └── workflows/ └── deploy.yml # build + cast on pushThree rules that keep this tidy:
infra/is the source of truth for everything that touches a server. Terraform, casts, values, registry config. Nothing deployable lives outside it.casts/*.yamlare templates. Every component-specific value comes fromvalues/<env>.yamlvia{{ values:foo.bar }}placeholders so the same cast renders for dev, staging, and prod.- Anything sensitive is
.example.yaml. Real secrets are created withrune create secretor applied from a local-only file. The example versions document the schema and ship in git.
Why this shape
Section titled “Why this shape”| Decision | Reason |
|---|---|
infra/terraform/do/ (not terraform/) | Leaves room for terraform/aws/, terraform/hetzner/, etc. without churn. |
One casts/<name>.yaml per component | Lets CI build + cast components independently. See part 4. |
values/<env>.yaml per environment | Same cast, different env. rune cast takes --values so you don’t fork YAML. |
*.example.yaml for secrets | Schema lives in git, values do not. The secrets guide covers the mechanics. |
Create the skeleton
Section titled “Create the skeleton”mkdir -p myapp/{apps/web,infra/terraform/do,infra/runeset/casts,infra/runeset/values,.github/workflows}cd myappgit initAdd a .gitignore that protects the obvious leaks:
# Terraform state and tfvars (anything that holds tokens)**/.terraform/**/terraform.tfstate***/terraform.tfvars**/*.token
# Local secret materializationsinfra/runeset/secrets.yamlCommit the empty skeleton so subsequent parts can be reviewed as small diffs:
git add . && git commit -m "infra: scaffold"That’s the whole step. The next four parts each fill in one slice of this tree.
Next: Part 2 — Provision with Terraform → · Back: Overview