Skip to content

Kubernetes

Doable ships a Kustomize-based deployment under deployment/platforms/k8s/. The base/ overlay deploys a single-replica stack suitable for staging. The overlays/prod/ overlay adds two replicas plus cert-manager TLS, and overlays/dev/ strips it back for a lab cluster.

Prerequisites

  • kubectl configured against your cluster.
  • ingress-nginx controller installed.
  • cert-manager installed (only for the prod overlay).
  • Cluster default storage class that supports ReadWriteOnce PVCs.

Deploy in five steps

# 1. Create the namespace
kubectl create namespace doable

# 2. Fill in secrets
cp deployment/platforms/k8s/base/secret.example.yaml \
   deployment/platforms/k8s/base/secret.yaml

# Edit secret.yaml. Every CHANGEME must be replaced:
#   JWT_SECRET / ENCRYPTION_KEY / INTERNAL_SECRET   openssl rand -hex 32
#   DOABLE_KEK                                      openssl rand -base64 32
#   POSTGRES_PASSWORD                               openssl rand -hex 16
#   DATABASE_URL                                    postgres://doable:<POSTGRES_PASSWORD>@postgres:5432/doable
#   INSTALL_BOOTSTRAP_TOKEN                         openssl rand -hex 32
#   INSTALL_BOOTSTRAP_TOKEN_EXPIRES_AT              ISO8601 (e.g. 2099-01-01T00:00:00Z)
# Set at least one AI provider key (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)

# 3. Enable secret.yaml in the kustomization
# Uncomment the "- secret.yaml" line in:
#   deployment/platforms/k8s/base/kustomization.yaml

# 4. Point configmap.yaml at your hostname
# Replace app.example.com in deployment/platforms/k8s/base/configmap.yaml

# 5. Apply
kubectl apply -k deployment/platforms/k8s/base/
# Or, for production with two replicas and cert-manager TLS:
kubectl apply -k deployment/platforms/k8s/overlays/prod/

Wait for rollout

kubectl wait --for=condition=available --timeout=300s deployment/api -n doable
kubectl wait --for=condition=available --timeout=300s deployment/ws  -n doable
kubectl wait --for=condition=available --timeout=300s deployment/web -n doable

kubectl get ingress -n doable

Open the ingress IP or hostname. The first signup at /auth/register is auto-promoted to platform owner.

What the manifests deploy

Files in deployment/platforms/k8s/base/:

File Resource
namespace.yaml doable namespace
configmap.yaml Public URL contract, ports, hostnames
secret.example.yaml Template for secret.yaml (gitignored)
postgres-statefulset.yaml Single-replica pgvector/pgvector:pg16 StatefulSet
postgres-service.yaml, postgres-pvc.yaml DB service + persistent volume claim
api-deployment.yaml, api-service.yaml, api-pvc.yaml API Hono server plus api-projects and api-thumbnails PVCs
ws-deployment.yaml, ws-service.yaml, ws-pvc.yaml WebSocket server (Yjs CRDT)
web-deployment.yaml, web-service.yaml Next.js frontend
ingress.yaml Single ingress that routes /api, /ws, and / to the right service
migrate-job.yaml Optional standalone migration Job for GitOps workflows

Overlays

Overlay Purpose
overlays/dev/ One replica each, reduced resource limits, plain HTTP. Lab clusters and CI.
overlays/prod/ Two replicas for api and web, cert-manager TLS via letsencrypt-prod, stricter limits.

Secret management

The default secret.yaml stores values as a plain Kubernetes Secret (base64-encoded, not encrypted unless your cluster has KMS). For production, choose one:

  • External Secrets Operator (recommended). Point a ClusterSecretStore at AWS Secrets Manager, GCP Secret Manager, or HashiCorp Vault.
  • sealed-secrets. kubeseal < secret.yaml > sealed-secret.yaml and commit the sealed copy.

Migrations

Migrations run automatically as an initContainer in each api and ws pod, before the main container starts. The migrate image is idempotent because every migration uses IF NOT EXISTS. A standalone migrate-job.yaml is also included for operators who prefer an explicit migration step, for instance with ArgoCD sync waves.

Scaling

  • web is stateless. Scale freely: kubectl scale deployment/web --replicas=3 -n doable.
  • api is stateful. Local PVCs (api-projects, api-thumbnails) are ReadWriteOnce. To scale above one replica, switch the PVCs to a ReadWriteMany storage class such as NFS, EFS, or CephFS.
  • ws requires sticky sessions for Yjs state. See the comment in ws-deployment.yaml.
  • postgres is a single-replica StatefulSet. For high availability, swap it for managed Postgres (RDS, Cloud SQL, Supabase) and update DATABASE_URL. The managed service must have pgvector, pg_trgm, and pgcrypto available.

Troubleshooting

Ingress has no address. Confirm an ingress controller is installed and watching the doable namespace. kubectl get pods -n ingress-nginx.

pgvector extension missing. The bundled StatefulSet uses pgvector/pgvector:pg16. If you point at managed Postgres, run CREATE EXTENSION IF NOT EXISTS vector; once as a privileged user.

TLS pending forever. The prod overlay uses cert-manager with the letsencrypt-prod ClusterIssuer. Confirm cert-manager is installed and that your DNS A record points at the ingress controller's external IP.

Migration initContainer crash-loops. Inspect kubectl logs <api-pod> -c migrate -n doable. Most failures are a stale Postgres volume from a previous install with a different POSTGRES_PASSWORD.

Reference

  • Source: deployment/platforms/k8s/README.md
  • Manifests: deployment/platforms/k8s/base/, overlays/dev/, overlays/prod/
  • Related: Custom Domains, Scaling