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/
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 together

Prerequisites

  • A Kubernetes cluster and a kubeconfig.yaml for 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:

images
ghcr.io/erip-me/messy-backend:v1.0.0
ghcr.io/erip-me/messy-frontend:v1.0.0

They 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:

terminal
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:latest

If 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:

deploy/prod/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    = "..."
}
FieldTypeDescription
namespacestringKubernetes namespace; created by the apply.
backend_host / frontend_hostreqstringPublic hostnames routed by the ingress.
backend_image / frontend_imagereqstringPublished release images, or your own build. Pin a version tag.
backend_secret_envreqmapSecret env for the API and worker: DATABASE_URL, SECRET_KEY_BASE, mail settings.
tls_secret_namestringTLS secret for the ingress; null when TLS terminates upstream.
image_pull_secret_namestringPull secret for a private registry; null for public images.
backend_replicas / frontend_replicasnumberPod counts, default 1.
terraform.tfvars and kubeconfig.yaml hold live credentials. They are gitignored in the repo; keep them that way.

Apply

terminal
cd deploy/prod
tofu init          # or: terraform init
tofu apply

The 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 a GET /up readiness 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.