Build Elevate

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

FilePurpose
k8s/namespace.ymlDedicated build-elevate namespace
k8s/configmap.ymlNon-secret config (NODE_ENV; API_INTERNAL_URL for fullstack)
k8s/api-deployment.ymlAPI Deployment with resource limits and readiness/liveness probes
k8s/api-service.ymlClusterIP Service for the API on port 4000
k8s/web-deployment.ymlWeb Deployment with resource limits and probes
k8s/web-service.ymlClusterIP Service for the web app on port 3000
k8s/api-ingress.ymlnginx Ingress routing /api/v1/* → API (with prefix rewrite)
k8s/web-ingress.ymlnginx Ingress routing / → web
k8s/api-hpa.ymlHorizontalPodAutoscaler (CPU/memory) for the API
k8s/web-hpa.ymlHorizontalPodAutoscaler (CPU/memory) for the web app
deploy.shBuild → push → apply pipeline (pnpm k8s:deploy)
k8s/verify.shPost-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

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.yaml

On 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 — the USERNAME variable
  • k8s/api-deployment.yml — the image: field
  • k8s/web-deployment.yml — the image: field
deploy.sh
USERNAME="your-dockerhub-username"
k8s/api-deployment.yml
image: your-dockerhub-username/your-project-api:latest

Make sure you are logged in to your registry locally so the script can push:

docker login

Step 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.production
  • apps/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:deploy

This will:

  1. Build the Docker image(s) from docker-compose.prod.yml
  2. Tag and push them to your registry
  3. Apply the namespace and ConfigMap
  4. Create/update the Secrets from your .env.production files
  5. Apply the Deployments, Services, Ingress, and HPAs
  6. Wait for each rollout to complete

Step 4: Verify

After deploying, run the verification script:

pnpm k8s:verify

It 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]-web

The 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:3000

Troubleshooting

  • HPA TARGETS shows <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 and docker login.
  • Service has no endpoints — the pods aren't ready; inspect them with kubectl get pods -n [your-project] and kubectl logs.

Need help? Check the Troubleshooting Guide or open an issue on GitHub.

On this page