DigitalOcean K8s runbook
Prod deploy of the plnt playground API to https://playground.plnt.work on a DigitalOcean Kubernetes cluster.
Naming:
plnt.work— static marketing site (Astro).plnt.work/playground— the chat UI island.playground.plnt.work— this API. Different origin from the UI on purpose.
Wall-clock: ~40 min first try. Monthly cost: ~$24 (1× s-1vcpu-2gb node $12 + 1× LB $12; DOCR free tier).
0. Prereqs (install once)
Section titled “0. Prereqs (install once)”brew install doctl kubectl helmdoctl auth initdoctl account getCloudflare API token (optional — the dashboard works too):
export CF_API_TOKEN=...1. Container registry — push the image
Section titled “1. Container registry — push the image”doctl registry create plnt --subscription-tier starterdoctl registry login
cd /Users/dev16/Documents/den-agent/plntdocker buildx build \ --platform linux/amd64 \ -f docker/playground-api.Dockerfile \ -t registry.digitalocean.com/plnt/playground-api:0.1.0 \ --push .
doctl registry repository list-v2If plnt is taken as a registry name, pick another (e.g. plnt-<yourname>) and update image.repository in deploy/do-k8s/values-do.yaml.
2. Cluster — create DOKS
Section titled “2. Cluster — create DOKS”doctl kubernetes cluster create plnt \ --region sfo3 \ --version latest \ --node-pool "name=default;size=s-1vcpu-2gb;count=1;auto-scale=true;min-nodes=1;max-nodes=3" \ --wait
kubectl config current-context # do-sfo3-plntkubectl get nodes # 1 node, Ready3. Registry pull secret
Section titled “3. Registry pull secret”kubectl create namespace plntdoctl registry kubernetes-manifest --namespace plnt --name docr-plnt | kubectl apply -f -kubectl -n plnt get secret docr-plnt4. Ingress controller — ingress-nginx
Section titled “4. Ingress controller — ingress-nginx”Provisions a DO LoadBalancer ($12/mo) as a side effect.
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginxhelm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx --create-namespace \ --set controller.publishService.enabled=true
kubectl -n ingress-nginx get svc ingress-nginx-controller -w# ^C when EXTERNAL-IP is not <pending>Copy the EXTERNAL-IP — call it $LB_IP.
5. cert-manager — Let’s Encrypt
Section titled “5. cert-manager — Let’s Encrypt”helm repo add jetstack https://charts.jetstack.iohelm repo update
helm install cert-manager jetstack/cert-manager \ --namespace cert-manager --create-namespace \ --set crds.enabled=true
kubectl -n cert-manager rollout status deploy/cert-managerkubectl apply -f deploy/do-k8s/cert-issuer.yamlTip: first deploy, flip
values-do.yamltoletsencrypt-staging. Confirm the flow, then switch toletsencrypt-prod.
6. DNS — Cloudflare
Section titled “6. DNS — Cloudflare”- Cloudflare dashboard →
plnt.workzone → DNS → Add record - Type
A, Nameplayground, IPv4$LB_IP, Proxy DNS only (grey cloud — orange proxy strips HTTP/1.1 upgrade for SSE)
dig +short playground.plnt.work # should return $LB_IP within a minute7. Ship the playground API
Section titled “7. Ship the playground API”cd /Users/dev16/Documents/den-agent/plnthelm install plnt-playground plnt/charts/playground-api \ --namespace plnt \ -f deploy/do-k8s/values-do.yaml
kubectl -n plnt rollout status deploy/plnt-playground-playground-apikubectl -n plnt get pods,svc,ingresskubectl -n plnt get certificate # READY: True after 30-90s8. Verify from the internet
Section titled “8. Verify from the internet”curl -s https://playground.plnt.work/v1/models | jq
curl -s https://playground.plnt.work/v1/chat/completions \ -H 'content-type: application/json' \ -d '{"model":"plnt-mock-7b","messages":[{"role":"user","content":"hello prod"}]}' | jq
# streamingcurl -sN https://playground.plnt.work/v1/chat/completions \ -H 'content-type: application/json' \ -d '{"model":"plnt-mock-7b","messages":[{"role":"user","content":"stream me"}],"stream":true}' \ | head -20Streaming response should arrive chunk-by-chunk (not batched — proves SSE passes through).
9. Point the site at the API
Section titled “9. Point the site at the API”In plnt-site, set PUBLIC_PLNT_ENDPOINT=https://playground.plnt.work as a Vercel env var and redeploy. The playground island falls back to a stub if the env var isn’t set, so the site is usable even when the API is down.
10. Upgrade / rollback
Section titled “10. Upgrade / rollback”docker buildx build --platform linux/amd64 \ -f docker/playground-api.Dockerfile \ -t registry.digitalocean.com/plnt/playground-api:0.1.1 \ --push .
helm upgrade plnt-playground plnt/charts/playground-api \ -n plnt -f deploy/do-k8s/values-do.yaml \ --set image.tag=0.1.1
# rollbackhelm history plnt-playground -n plnthelm rollback plnt-playground <revision> -n plnt11. Teardown
Section titled “11. Teardown”helm uninstall plnt-playground -n plnthelm uninstall ingress-nginx -n ingress-nginx # frees the LBhelm uninstall cert-manager -n cert-managerdoctl kubernetes cluster delete plnt # frees the nodedoctl registry delete plntCommon failures
Section titled “Common failures”| Symptom | Fix |
|---|---|
exec format error in pod logs | Image built for arm64 on Apple silicon — rebuild with --platform linux/amd64. |
ImagePullBackOff for registry.digitalocean.com/... | docr-plnt secret missing in plnt namespace. Re-run step 3. |
certificate stuck Ready: False | DNS not resolving yet — Let’s Encrypt HTTP-01 needs playground.plnt.work reachable. |
| SSE responses arrive all at once | Cloudflare orange-cloud proxy buffering. Flip to grey. |