Deploying to Kubernetes
Deploy the web and API applications to any Kubernetes cluster using the included manifests, deploy script, and verification script
Overview
build-elevate ships a complete set of Kubernetes manifests in the k8s/ directory, plus a deploy.sh script that builds and pushes your Docker images and applies the manifests, and a verify.sh script that checks the rollout afterward.
The manifests are template-aware: a full-stack project gets both web and API resources, while web-only and api-only projects ship only the manifests for the app they include.
What's included
| File | Purpose |
|---|---|
k8s/namespace.yml | Dedicated build-elevate namespace |
k8s/configmap.yml | Non-secret config (NODE_ENV; API_INTERNAL_URL for fullstack) |
k8s/api-deployment.yml | API Deployment with resource limits and readiness/liveness probes |
k8s/api-service.yml | ClusterIP Service for the API on port 4000 |
k8s/web-deployment.yml | Web Deployment with resource limits and probes |
k8s/web-service.yml | ClusterIP Service for the web app on port 3000 |
k8s/api-ingress.yml | nginx Ingress routing /api/v1/* → API (with prefix rewrite) |
k8s/web-ingress.yml | nginx Ingress routing / → web |
k8s/api-hpa.yml | HorizontalPodAutoscaler (CPU/memory) for the API |
k8s/web-hpa.yml | HorizontalPodAutoscaler (CPU/memory) for the web app |
deploy.sh | Build → push → apply pipeline (pnpm k8s:deploy) |
k8s/verify.sh | Post-deploy health/rollout checks (pnpm k8s:verify) |
Kubernetes support is opt-in. The scaffolder asks "Include Kubernetes
manifests?" (only when Docker is enabled, since the manifests run the Docker
images). If you decline — or opt out of Docker — the k8s/ directory and
deploy.sh are not generated.
Prerequisites
- A running Kubernetes cluster (cloud-managed, or local via minikube / kind)
kubectlconfigured to talk to your cluster- A container registry account (the scripts assume Docker Hub)
- An ingress-nginx controller installed in the cluster
- metrics-server installed (required for the HPAs to scale)
If your cluster doesn't already have them, install the ingress controller and metrics-server:
# nginx Ingress controller
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.1/deploy/static/provider/cloud/deploy.yaml
# Metrics Server (required for HPA — without it the HPAs read <unknown> and cannot scale)
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yamlOn minikube, enable the bundled metrics-server addon instead: minikube addons enable metrics-server.
Step 1: Set your registry username
The deploy script and deployment manifests use the placeholder your-dockerhub-username. Replace it with your own Docker Hub username in:
deploy.sh— theUSERNAMEvariablek8s/api-deployment.yml— theimage:fieldk8s/web-deployment.yml— theimage:field
USERNAME="your-dockerhub-username"image: your-dockerhub-username/your-project-api:latestMake sure you are logged in to your registry locally so the script can push:
docker loginStep 2: Create production environment files
Secrets are created from per-app .env.production files. If you scaffolded with Docker, these were generated for you — fill them in with real production values:
apps/api/.env.productionapps/web/.env.production
deploy.sh turns each into a Kubernetes Secret ([your-project]-api-env, [your-project]-web-env) that the deployments load via envFrom.
Never commit .env.production files. They contain database credentials, auth
secrets, and API keys.
Step 3: Deploy
Run the deploy script from the project root:
pnpm k8s:deployThis will:
- Build the Docker image(s) from
docker-compose.prod.yml - Tag and push them to your registry
- Apply the namespace and ConfigMap
- Create/update the Secrets from your
.env.productionfiles - Apply the Deployments, Services, Ingress, and HPAs
- Wait for each rollout to complete
Step 4: Verify
After deploying, run the verification script:
pnpm k8s:verifyIt works outward — pods → deployments → services/endpoints → in-cluster HTTP → ingress → HPA — and exits non-zero if any hard check fails, so it can be used in CI.
How it works
Autoscaling owns replica count
The Deployments intentionally omit replicas. The per-app HorizontalPodAutoscaler (k8s/api-hpa.yml, k8s/web-hpa.yml) owns that value (min 2, max 10, scaling on CPU/memory). Setting replicas on the Deployment would reset the pod count on every kubectl apply and fight the autoscaler.
Ingress routing
The ingress mirrors the local dev rewrite: requests to /api/v1/* are stripped to /api/* and routed to the API service (k8s/api-ingress.yml), while everything else goes to the web app (k8s/web-ingress.yml).
Internet → nginx Ingress (:80/:443)
├─ /api/v1/* → rewrite /api/v1/* → /api/* → build-elevate-api-service (:4000) → Express API
└─ /* → build-elevate-web-service (:3000) → Next.js pages
Next.js server-side fetches (SSR) reach the API in-cluster via
API_INTERNAL_URL → build-elevate-api-service (:4000), bypassing the ingress.Next.js server-side fetches (SSR) don't go back out through the ingress — they reach the API directly over the cluster network via API_INTERNAL_URL (set in k8s/configmap.yml), pointed at the API's ClusterIP service.
Custom domain and TLS
The manifests ship hostless (catch-all), so they work as-is against the ingress controller's external IP. For a real domain, add a host: to each rule in k8s/api-ingress.yml and k8s/web-ingress.yml.
To enable TLS, install cert-manager, then uncomment the tls: block and the cert-manager.io/cluster-issuer annotation in those ingress files.
Health probes
The API uses /readyz (readiness) and /healthz (liveness); the web app probes /. These match the endpoints exposed by the Express API (/healthz, /readyz, /health).
Useful commands
Inspect the running deployment (replace [your-project] with your namespace):
kubectl get pods -n [your-project]
kubectl get services -n [your-project]
kubectl get ingress -n [your-project]
kubectl get hpa -n [your-project]
kubectl logs -n [your-project] -l app=[your-project]-api
kubectl logs -n [your-project] -l app=[your-project]-webThe API health endpoints (/health, /healthz, /readyz) live at the API root and are not exposed through the ingress, so port-forwarding is how you reach them locally:
# Ctrl+C to stop the tunnel
kubectl port-forward -n [your-project] svc/[your-project]-api-service 4000:4000 # → http://localhost:4000/health
kubectl port-forward -n [your-project] svc/[your-project]-web-service 3000:3000 # → http://localhost:3000Troubleshooting
- HPA
TARGETSshows<unknown>— metrics-server isn't serving metrics. Install it (on minikube:minikube addons enable metrics-server). ImagePullBackOff— the image wasn't pushed or the registry username in the manifests doesn't match what you pushed. Re-check Step 1 anddocker login.- Service has no endpoints — the pods aren't ready; inspect them with
kubectl get pods -n [your-project]andkubectl logs.
Need help? Check the Troubleshooting Guide or open an issue on GitHub.
Deploying to Vercel
Deploy Next.js web and API applications to Vercel with automatic optimizations, environment configuration, and seamless monorepo integration
Troubleshooting
Solutions for common issues including port conflicts, dependency problems, database errors, and environment configuration errors