Deploy with Terraform
Run Messy on Kubernetes with the Terraform modules from the repo: the same setup that serves messy.sh, driven by a handful of variables.
The deploy/ directory contains Terraform for a production Kubernetes deployment. It works with Terraform 1.6+ and equally with OpenTofu (that is what we run ourselves). The modules create plain Kubernetes resources through the official provider; there is no cloud-vendor lock-in beyond a cluster and a Postgres server.
deploy/
modules/
backend/ # API: deployment, service, config, secret, storage claim
frontend/ # web app: deployment, service
worker/ # Solid Queue processor: deployment
ingress/ # one ingress for both hosts, TLS optional
prod/ # an environment: wires the modules togetherPrerequisites
- A Kubernetes cluster and a
kubeconfig.yamlfor it, with an NGINX ingress controller installed. - A Postgres 14+ database reachable from the cluster. Solid Queue and Solid Cable share it; no Redis.
- Optional: a container registry, only if you build your own images rather than using the published ones.
- Optional: a TLS certificate stored as a Kubernetes secret, if TLS terminates at the ingress rather than at a proxy in front.
Images
Every release publishes two public images, so there is nothing to build:
ghcr.io/erip-me/messy-backend:v1.0.0
ghcr.io/erip-me/messy-frontend:v1.0.0They are public, so the cluster pulls them without credentials and image_pull_secret_name stays null. Pin a version tag rather than latest: with imagePullPolicy: Always on the deployments, a floating tag means a pod restart can quietly land on a different release.
Prefer to build your own, from a fork or a patched tree? Push them anywhere the cluster can reach and use those references instead:
docker build -t registry.example.com/messy/backend:latest backend/
docker build -t registry.example.com/messy/frontend:latest frontend/
docker push registry.example.com/messy/backend:latest
docker push registry.example.com/messy/frontend:latestIf that registry is private, create an image pull secret in the target namespace and pass its name as image_pull_secret_name.
Configure an environment
Copy deploy/prod/ as your starting point, drop your kubeconfig.yaml next to it, and write a terraform.tfvars:
kubeconfig_context = "my-cluster"
namespace = "messy"
backend_host = "api.example.com"
frontend_host = "app.example.com"
backend_image = "ghcr.io/erip-me/messy-backend:v1.0.0"
frontend_image = "ghcr.io/erip-me/messy-frontend:v1.0.0"
tls_secret_name = "messy-tls" # or null
image_pull_secret_name = null # only for a private registry
backend_secret_env = {
DATABASE_URL = "postgres://messy:...@db.internal:5432/messy"
SECRET_KEY_BASE = "..." # openssl rand -hex 64
EMAIL_FROM = "messy@example.com"
SMTP_ADDRESS = "smtp.example.com"
SMTP_USERNAME = "..."
SMTP_PASSWORD = "..."
}| Field | Type | Description |
|---|---|---|
namespace | string | Kubernetes namespace; created by the apply. |
backend_host / frontend_hostreq | string | Public hostnames routed by the ingress. |
backend_image / frontend_imagereq | string | Published release images, or your own build. Pin a version tag. |
backend_secret_envreq | map | Secret env for the API and worker: DATABASE_URL, SECRET_KEY_BASE, mail settings. |
tls_secret_name | string | TLS secret for the ingress; null when TLS terminates upstream. |
image_pull_secret_name | string | Pull secret for a private registry; null for public images. |
backend_replicas / frontend_replicas | number | Pod counts, default 1. |
terraform.tfvars and kubeconfig.yaml hold live credentials. They are gitignored in the repo; keep them that way.Apply
cd deploy/prod
tofu init # or: terraform init
tofu applyThe apply creates the namespace, a deployment plus service for the backend and the frontend, a worker deployment, a shared storage claim for uploads, and one ingress covering both hosts. Point DNS for the two hostnames at your ingress and you are live.
How it runs
- Backend and worker start with
rails db:prepare, so schema setup and migrations happen on boot. Rolling updates wait on aGET /upreadiness probe, so a failed migration never takes traffic. - Uploads live on a shared volume mounted by the backend and the worker; the claim survives redeploys.
- Config changes are Terraform changes: edit tfvars,
tofu apply, then restart the deployments so pods pick up the new config:kubectl rollout restart deployment/backend deployment/frontend deployment/worker -n messy.
Prefer a single machine over a cluster? The Docker Compose deployment is the same stack without Kubernetes.