This guide deploys Terrapod onto a Kubernetes cluster with the Helm chart, then creates your first workspace and runs your first plan and apply against it.
Terrapod runs only on Kubernetes — the runner uses the Jobs API to schedule plan/apply executions. You don't need a managed cloud cluster; a single-node k3s VM works. If you instead want to run Terrapod from source for development, see Local Development.
OpenTofu is the recommended execution backend. Terrapod supports both OpenTofu (
tofu) and Terraform (terraform) as execution backends. OpenTofu is recommended because it is open-source under the MPL-2.0 license. All CLI examples in this guide usetofu, butterraformcommands work identically.
| Need | Notes |
|---|---|
| A Kubernetes cluster (1.27+) | Any conformant cluster. No cluster? Spin up k3s on a single VM (below). |
| Helm 3.x | brew install helm |
| PostgreSQL 14+ and Redis 7+ | External — the chart does not bundle them. Provide a connection URL for each (a managed service, or run them on the cluster/VM). See Deployment → Database Setup. |
| Storage | Defaults to a PVC-backed filesystem (no extra setup). For S3/Azure/GCS see Deployment → Storage Backend Setup. |
tofu (recommended) or terraform |
Infrastructure CLI — opentofu.org or terraform.io. |
k3s is a fully conformant single-binary Kubernetes that runs on one VM (~512 MB RAM) and ships with an ingress controller (Traefik) and local-path storage:
curl -sfL https://get.k3s.io | sh - # single-node cluster
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bashRun PostgreSQL and Redis as managed services or on a separate VM so your data survives cluster recreation.
Terrapod is published to GitHub Container Registry as an OCI Helm chart at oci://ghcr.io/mattrobinsonsre/terrapod.
Pick a hostname and install:
export TERRAPOD_HOST=terrapod.example.com
helm install terrapod oci://ghcr.io/mattrobinsonsre/terrapod \
--namespace terrapod --create-namespace \
--set ingress.enabled=true \
--set ingress.hostname="$TERRAPOD_HOST" \
--set ingress.className=traefik \
--set postgresql.url="postgresql+asyncpg://terrapod:PASSWORD@PGHOST:5432/terrapod" \
--set redis.url="redis://REDISHOST:6379" \
--set bootstrap.adminEmail="admin@example.com" \
--set bootstrap.adminPassword="change-me-now"What the defaults give you: filesystem storage on a PVC, local password auth enabled, a pre-install migrations job, and a post-install bootstrap job that creates the admin user from bootstrap.adminEmail / bootstrap.adminPassword. Set ingress.className to your cluster's ingress controller (traefik on k3s, often nginx on managed clusters). Pin a version with --version X.Y.Z.
For production, put the admin password in a Kubernetes Secret (
bootstrap.existingSecret) rather than on the command line, terminate TLS at the ingress (e.g. cert-manager), and setapi.config.external_urlso outbound links (VCS status, notifications) are correct. The full value reference — storage backends, external DB, SSO, scaling, TLS, runners — is in Deployment, and Production Checklist covers hardening.
Watch it come up:
kubectl -n terrapod get pods -wPoint your hostname's DNS at the ingress controller's external IP (on k3s, the VM's IP — Traefik listens on ports 80/443). For a quick local test you can add a hosts entry instead:
sudo sh -c 'echo "<INGRESS_IP> terrapod.example.com" >> /etc/hosts'Then open https://terrapod.example.com and log in with the bootstrap admin email + password.
TLS: the ingress expects a certificate by default (
ingress.tls=true). For a quick HTTP-only evaluation, add--set ingress.tls=false; for real TLS see Deployment → Ingress.No DNS available at all? Port-forward the web service for a quick look:
kubectl -n terrapod port-forward svc/terrapod-web 8080:3000, then openhttp://localhost:8080.
In the web UI, go to Settings → API Tokens, create a token, and copy the value (shown once):
export TERRAPOD_TOKEN="<your-token-value>"Or let the CLI mint one for you via the browser login flow:
tofu login terrapod.example.comcurl -s -X POST https://terrapod.example.com/api/v2/organizations/default/workspaces \
-H "Authorization: Bearer $TERRAPOD_TOKEN" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"type": "workspaces",
"attributes": {
"name": "demo",
"auto-apply": true
}
}
}' | jq .Note the workspace ID from the response (e.g., ws-abc123...).
- Navigate to Workspaces
- Click New Workspace
- Enter a name (e.g.,
demo) - Optionally enable auto-apply
- Click Create
Create a configuration that uses Terrapod as its backend:
# main.tf
terraform {
cloud {
hostname = "terrapod.example.com"
organization = "default"
workspaces {
name = "demo"
}
}
}
# A simple example resource
resource "null_resource" "hello" {
triggers = {
timestamp = timestamp()
}
provisioner "local-exec" {
command = "echo 'Hello from Terrapod!'"
}
}Note: The
terraform {}block syntax is used by both OpenTofu and Terraform. No changes are needed when switching between them.
Do not set
project = "..."in the cloud block. Terrapod is single-organization and has no project concept — workspaces live directly under the organization. Settingprojectcausestofu initto fail with a422 Projects are not supportederror from the API. Omit the argument.If you used TFC projects to scope RBAC (e.g. "team X has write on project Y"), Terrapod's label-based RBAC covers the same use case and more flexibly: roles match workspaces by label allow/deny rules (e.g.
team: payments+env: prod→write), so a workspace can belong to any combination of dimensions instead of a single project. The same labels also drive filtering in the web UI — workspace lists, registry modules, and other resources can be filtered by any label key/value, so labels double as the navigation hierarchy projects provided in TFC. Seedocs/rbac.mdfor examples.
If you'd rather select a workspace at run time (typical when a single repo backs several environments) replace name with tags. Tags are matched against the workspace's labels — see the RBAC docs for details.
workspaces {
tags = ["network"] # any workspace with the label key "network"
# or, for an exact key:value match:
tags = ["repo:aws-infra"] # workspaces whose `repo` label is `aws-infra`
}OpenTofu only accepts the set-of-strings
tagsform, so use thekey:valuestring above for an exact match. The Terraform 1.10+ map form (tags = { repo = "aws-infra" }) is rejected bytofuat config validation withset of string required.
Then pick a specific workspace per invocation:
TF_WORKSPACE=network-staging tofu plan
# or
tofu workspace select network-stagingIf you have not already run tofu login:
tofu login terrapod.example.comThen initialize:
tofu initYou should see output confirming that OpenTofu is using Terrapod as its backend:
Initializing Terraform Cloud...
...
Terraform Cloud has been successfully initialized!
In local execution mode, tofu runs on your machine and pushes state to Terrapod:
tofu planThe plan output appears in your terminal as usual. State locking is handled by Terrapod (lock/unlock API calls).
tofu applyState is uploaded to Terrapod after a successful apply. You can view state versions in the web UI under the workspace's State tab.
For agent execution, the runner listener creates a K8s Job that runs tofu (or terraform) on the server via an agent pool.
Agent workspaces support two workflows depending on whether VCS is connected:
VCS-connected workspaces — VCS is the source of truth. CLI can only plan; applies must come through VCS.
Non-VCS workspaces (CLI-driven) — the CLI is the source of truth. tofu plan and tofu apply both work, with the apply phase running on the server after plan confirmation.
| Source | VCS-Connected | Non-VCS (CLI-driven) |
|---|---|---|
tofu plan (CLI) |
Plan-only on server | Plan-only on server |
tofu apply (CLI) |
Blocked (use VCS) | Plan + confirm + apply on server |
| VCS push to tracked branch | Full plan + apply | N/A |
| VCS pull request / merge request | Speculative plan-only | N/A |
- Set the workspace execution mode to
agent:
curl -X PATCH https://terrapod.example.com/api/v2/workspaces/ws-{id} \
-H "Authorization: Bearer $TERRAPOD_TOKEN" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"type": "workspaces",
"attributes": {
"execution-mode": "agent"
}
}
}'- Create a configuration version and upload your code:
# Create configuration version
CV_RESPONSE=$(curl -s -X POST https://terrapod.example.com/api/v2/workspaces/ws-{id}/configuration-versions \
-H "Authorization: Bearer $TERRAPOD_TOKEN" \
-H "Content-Type: application/vnd.api+json" \
-d '{"data": {"type": "configuration-versions", "attributes": {"auto-queue-runs": true}}}')
# Extract upload URL
UPLOAD_URL=$(echo "$CV_RESPONSE" | jq -r '.data.attributes."upload-url"')
# Create tarball and upload
tar -czf config.tar.gz -C /path/to/terraform/dir .
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/octet-stream" \
--data-binary @config.tar.gz- A run is automatically queued (plan-only). Monitor it:
curl -s https://terrapod.example.com/api/v2/workspaces/ws-{id}/runs \
-H "Authorization: Bearer $TERRAPOD_TOKEN" | jq '.data[0].attributes.status'Variables can be set per-workspace via the API or web UI.
curl -X POST https://terrapod.example.com/api/v2/workspaces/ws-{id}/vars \
-H "Authorization: Bearer $TERRAPOD_TOKEN" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"type": "vars",
"attributes": {
"key": "instance_type",
"value": "t3.micro",
"category": "terraform",
"sensitive": false,
"description": "EC2 instance type"
}
}
}'curl -X POST https://terrapod.example.com/api/v2/workspaces/ws-{id}/vars \
-H "Authorization: Bearer $TERRAPOD_TOKEN" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"type": "vars",
"attributes": {
"key": "AWS_REGION",
"value": "eu-west-1",
"category": "env",
"sensitive": false
}
}
}'Set "sensitive": true for secrets. The value is protected by database encryption-at-rest and never returned in API responses:
curl -X POST https://terrapod.example.com/api/v2/workspaces/ws-{id}/vars \
-H "Authorization: Bearer $TERRAPOD_TOKEN" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"type": "vars",
"attributes": {
"key": "AWS_SECRET_ACCESS_KEY",
"value": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"category": "env",
"sensitive": true
}
}
}'Note: Sensitive variables are protected by database encryption-at-rest and never returned in API responses.
For managing variables across multiple workspaces, see variable sets (admin-only) in the web UI under Admin > Variable Sets.
- Create the module:
curl -X POST https://terrapod.example.com/api/terrapod/v1/registry-modules \
-H "Authorization: Bearer $TERRAPOD_TOKEN" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"type": "registry-modules",
"attributes": {
"name": "vpc",
"provider": "aws"
}
}
}'- Create a version:
curl -X POST https://terrapod.example.com/api/terrapod/v1/registry-modules/private/default/vpc/aws/versions \
-H "Authorization: Bearer $TERRAPOD_TOKEN" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"type": "registry-module-versions",
"attributes": {
"version": "1.0.0"
}
}
}'-
Upload the module tarball using the presigned URL from the response.
-
Use the module in Terraform:
module "vpc" {
source = "terrapod.example.com/default/vpc/aws"
version = "1.0.0"
}For full registry documentation, see registry.md.
To automatically trigger runs when you push to a Git repository:
- Create a VCS connection (GitHub or GitLab)
- Link a workspace to a repository
See vcs-integration.md for detailed setup instructions.
helm uninstall terrapod --namespace terrapodPersistentVolumeClaims (filesystem storage, ephemeral runner scratch) are not
removed by helm uninstall — delete them explicitly if you want to reclaim the
storage, and drop the namespace with kubectl delete namespace terrapod.
Building and testing Terrapod from source — Tilt, live-reload,
make test— is covered in Local Development. You don't need any of that to run Terrapod.
- Authentication -- configure OIDC/SAML identity providers
- RBAC -- set up roles and permissions
- Drift Detection -- enable scheduled infrastructure drift checks
- Notifications -- set up Slack, webhook, or email alerts on run events
- Run Triggers -- create cross-workspace dependency chains
- Run Tasks -- add pre/post-plan validation webhooks
- Deployment -- deploy to production
- API Reference -- full API documentation



