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¶
kubectlconfigured against your cluster.ingress-nginxcontroller installed.cert-managerinstalled (only for theprodoverlay).- Cluster default storage class that supports
ReadWriteOncePVCs.
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
ClusterSecretStoreat AWS Secrets Manager, GCP Secret Manager, or HashiCorp Vault. - sealed-secrets.
kubeseal < secret.yaml > sealed-secret.yamland 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) areReadWriteOnce. To scale above one replica, switch the PVCs to aReadWriteManystorage 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 havepgvector,pg_trgm, andpgcryptoavailable.
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