This guide covers deploying the application to a production Kubernetes cluster using the manifests in k8s/.
Before deploying, ensure you have the following available:
- kubectl — the Kubernetes CLI, matching (or within one minor version of) your cluster. Verify with
kubectl version --client. - kustomize — used to build the manifests.
kubectlships withkustomizebuilt in (kubectl kustomize), but a standalonekustomizebinary is also supported. Verify withkustomize version. - A Kubernetes cluster — with a valid
kubectlcontext configured. Confirm withkubectl config current-contextandkubectl cluster-info. - An ingress controller — e.g. ingress-nginx. The
Ingressresource ink8s/assumes an ingress controller is installed and watchingIngressresources. - cert-manager — for automatic TLS certificate issuance. Install it and configure a
ClusterIssuer(e.g. Let's Encrypt) before applying the ingress. See the cert-manager docs.
Everything lives in the synapsvault namespace (set by k8s/kustomization.yaml):
| Resource | Purpose |
|---|---|
Deployment/backend |
API server on port 3000, with a migrate initContainer that runs drizzle-kit migrate before each new version starts |
Service/backend, HorizontalPodAutoscaler/backend |
ClusterIP on port 3000; scales 2–10 replicas on CPU |
Deployment/frontend, Service/frontend |
nginx serving the built SPA on port 80 |
ConfigMap/backend-config |
Non-secret backend settings (network, RPC URLs, CORS origins) |
Ingress/app-api |
https://<host>/api/* → backend, with the /api prefix stripped |
Ingress/app-web |
https://<host>/* → frontend |
Before the first deploy, replace app.example.com in k8s/ingress.yaml and ALLOWED_ORIGINS in k8s/configmap.yaml with your real host.
Images are built from this repo's backend/Dockerfile and frontend/Dockerfile with the SynapsVault/backend and SynapsVault/frontend repositories as build contexts, and pushed to ghcr.io/synapsvault/deploy/{backend,backend-migrate,frontend}.
The backend reads its secrets from a Secret named backend-secret, which must exist before the workloads are applied. Create it out-of-band so real values are never committed. k8s/secrets.yaml documents the expected keys; it is deliberately not part of the kustomization, so kubectl apply -k never overwrites the real secret with placeholders.
Create the namespace first (if it does not already exist):
kubectl create namespace synapsvaultCreate the secret from an env file (recommended, so values are not left in shell history), using the keys listed in k8s/secrets.yaml:
kubectl create secret generic backend-secret \
--namespace synapsvault \
--from-env-file=./secrets.envIf you need to update an existing secret:
kubectl create secret generic backend-secret \
--namespace synapsvault \
--from-env-file=./secrets.env \
--dry-run=client -o yaml | kubectl apply -f -Note: After rotating secrets, restart the workloads so the new values are picked up:
kubectl rollout restart deployment/backend -n synapsvault.
Build and apply everything under k8s/ with kustomize:
kubectl apply -k k8s/To preview what will be applied without changing the cluster:
kubectl kustomize k8s/The manifests reference images by the :latest tag. To deploy a specific build, pin the tags first (this is what CI does):
cd k8s
kustomize edit set image \
backend=ghcr.io/synapsvault/deploy/backend:<sha> \
backend-migrate=ghcr.io/synapsvault/deploy/backend-migrate:<sha> \
frontend=ghcr.io/synapsvault/deploy/frontend:<sha>The Deploy to Kubernetes workflow runs on pushes to main that touch backend/, frontend/ or k8s/ (or on manual dispatch, where you can choose the app repo refs). It builds and pushes all three images tagged with the commit SHA, pins those tags, applies the kustomization and waits for both rollouts.
It needs a base64-encoded kubeconfig in the KUBECONFIG secret of the production environment (base64 -w0 < kubeconfig). Until that secret exists, the deploy job is skipped with a warning; images are still built and pushed.
Watch the rollout status of each deployment:
kubectl rollout status deployment/<name> -n synapsvaultInspect the resulting resources:
kubectl get all -n synapsvault
kubectl get ingress -n synapsvault
kubectl get pods -n synapsvault -o wideCheck that pods are Running and Ready, and that the ingress has an address assigned. If a pod is not becoming ready, inspect it:
kubectl describe pod <pod> -n synapsvault
kubectl logs <pod> -n synapsvaultScale a deployment manually:
kubectl scale deployment/<name> --replicas=5 -n synapsvaultNote: If a HorizontalPodAutoscaler (HPA) manages the deployment, manual scaling will be overridden by the HPA. Adjust the HPA's
minReplicas/maxReplicasinstead (see below).
Every kubectl apply of a changed pod template creates a new ReplicaSet, giving you a revision history to roll back through.
View the rollout history:
kubectl rollout history deployment/<name> -n synapsvaultRoll back to the previous revision:
kubectl rollout undo deployment/<name> -n synapsvaultRoll back to a specific revision:
kubectl rollout undo deployment/<name> --to-revision=<n> -n synapsvaultConfirm the rollback completed:
kubectl rollout status deployment/<name> -n synapsvaultThe HorizontalPodAutoscaler in k8s/ scales the deployment based on CPU utilization. Key fields:
scaleTargetRef— points at the deployment to scale.minReplicas— the floor; the deployment never scales below this.maxReplicas— the ceiling; the deployment never scales above this.metrics— typicallytype: Resource,resource.name: cpu, with atarget.averageUtilizationpercentage (e.g.70).
The HPA requires the metrics-server to be installed in the cluster; without it, the HPA cannot read utilization and will not scale. Verify metrics are available:
kubectl top pods -n synapsvault
kubectl get hpa -n synapsvaultTo change scaling behavior, edit the HPA (or its manifest in k8s/) and re-apply:
kubectl edit hpa <name> -n synapsvaultThe deployment configures liveness and readiness probes so Kubernetes can detect unhealthy pods and route traffic only to healthy ones:
livenessProbe— restarts the container if the check fails repeatedly. Points at a lightweight health endpoint (e.g./healthz).readinessProbe— removes the pod from Service endpoints until the check passes, so traffic is only sent to pods that can serve requests (e.g./readyz).startupProbe(if configured) — gives slow-starting containers time to boot before liveness checks begin.
Each probe sets:
httpGet.path/httpGet.port— the endpoint and port to probe.initialDelaySeconds— how long to wait before the first probe.periodSeconds— how often to probe.timeoutSeconds— how long to wait for a response.failureThreshold/successThreshold— how many consecutive failures/successes trigger a state change.
Inspect probe behavior:
kubectl describe pod <pod> -n synapsvaultLook at the Liveness/Readiness sections and the Events at the bottom for probe failures.
- Pods stuck in
Pending— check node capacity and scheduling events:kubectl describe pod <pod> -n synapsvault. - Pods in
CrashLoopBackOff— check logs:kubectl logs <pod> -n synapsvault --previous. - Ingress has no address — confirm an ingress controller is installed and running.
- TLS certificate not issued — check cert-manager:
kubectl describe certificate -n synapsvaultandkubectl describe certificaterequest -n synapsvault. - HPA shows
<unknown>targets — metrics-server is missing or not ready.