Strapi on k3s (Production Setup)¶
This document describes a production‑grade Strapi installation on a k3s cluster using:
- PostgreSQL (existing, external)
- Envoy Gateway (Gateway API)
- cert-manager + Let's Encrypt
- GitHub Container Registry (GHCR)
- GitHub Actions CI/CD
Domain: - https://cms.hershkowitz.co.il
1. Overview¶
Strapi is deployed as a containerized Node.js application, built from source, pushed to GHCR, and deployed into Kubernetes. Ingress is handled by Envoy Gateway with TLS termination.
2. Architecture¶
Internet
→ Envoy Gateway (HTTPS, SNI)
→ HTTPRoute (cms.hershkowitz.co.il)
→ Strapi Service (cms/strapi:1337)
→ PostgreSQL
→ PersistentVolume (uploads)
3. Namespace¶
kubectl create namespace cms
4. Database¶
PostgreSQL is pre‑existing.
Created manually:
- Database: strapi
- User: strapi
- Password stored in Kubernetes Secret
5. Persistent Storage (Uploads)¶
Critical path for Strapi uploads:
/app/public/uploads
PVC:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: strapi-uploads
namespace: cms
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
6. Secrets¶
Secret: strapi-secrets (namespace cms)
Includes: - DATABASE_* (PostgreSQL) - APP_KEYS - API_TOKEN_SALT - ADMIN_JWT_SECRET - JWT_SECRET - HOST=0.0.0.0 - PORT=1337 - PUBLIC_URL=https://cms.hershkowitz.co.il
7. Strapi App Creation¶
Created using Node container:
docker run --rm -it -v "$PWD/k8s/strapi/app:/app" -w /app node:20-bullseye bash -lc "npx create-strapi-app@latest ."
Options: - TypeScript: YES - Database: PostgreSQL - Git init: NO
8. Docker Image¶
Do NOT use strapi/strapi:latest.
Strapi must be built from your app source.
Dockerfile:
FROM node:20-bullseye AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-bullseye
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app ./
EXPOSE 1337
CMD ["npm","run","start"]
9. CI/CD (GitHub Actions → GHCR)¶
Workflow:
.github/workflows/strapi-build-push.yml
Responsibilities:
- Build image
- Push to GHCR
- Create ghcr-credentials secret in cms namespace
Image pushed as:
ghcr.io/rotem-blik/strapi:1
10. Kubernetes Deployment¶
Key points:
- Uses imagePullSecrets: ghcr-credentials
- Mounts PVC to /app/public/uploads
- Reads env vars from strapi-secrets
11. Ingress (Envoy Gateway)¶
HTTPRoute:
hostnames:
- cms.hershkowitz.co.il
parentRefs:
- name: public-gw
namespace: default
12. TLS / cert-manager¶
Certificate must be in default namespace (same as Gateway):
kind: Certificate
metadata:
name: cms-hershkowitz-co-il
namespace: default
spec:
secretName: cms-hershkowitz-co-il-tls
issuerRef:
name: letsencrypt
kind: ClusterIssuer
dnsNames:
- cms.hershkowitz.co.il
Gateway listener:
- name: https-cms
hostname: cms.hershkowitz.co.il
tls:
certificateRefs:
- name: cms-hershkowitz-co-il-tls
13. Verification Checklist¶
- Pod running:
cms/strapi - HTTPS works
/adminloads- Upload survives pod restart
14. Real Issues Encountered (Important)¶
- ❌
strapi/strapi:latestnot pullable - ❌ Missing
package-lock.jsonbreaksnpm ci - ❌ Wrong GHCR owner (
rotemh-blikvsrotem-blik) - ❌ Wrong upload mount path (
/opt/appinstead of/app) - ❌ TLS secret in wrong namespace
15. Prod to Dev and Back¶
Strapi – Switching Between Development and Production (Kubernetes)¶
This document explains how to safely switch a Strapi instance between Development and Production modes when running on Kubernetes.
It is written for a single Strapi deployment that you temporarily flip to Development for schema editing, and then return to Production.
Assumptions¶
- Kubernetes namespace:
cms - Deployment name:
strapi - Container name:
strapi - Strapi runs from an image whose Dockerfile uses:
CMD ["npm", "run", "start"]
What the Modes Mean¶
Production Mode¶
NODE_ENV=production- Strapi runs with
npm run start - Admin schema editing is disabled
- Intended for real users / traffic
Development Mode¶
NODE_ENV=development- Strapi runs with
npm run develop --watch-admin - Content-Type Builder and schema editing are enabled
- Intended for short, controlled use only
Switch Strapi to Production Mode¶
1. Set NODE_ENV=production¶
kubectl -n cms set env deployment/strapi NODE_ENV=production
2. Run Strapi using npm run start¶
kubectl -n cms patch deployment strapi --type='json' -p='[
{
"op": "replace",
"path": "/spec/template/spec/containers/0/command",
"value": ["npm"]
},
{
"op": "replace",
"path": "/spec/template/spec/containers/0/args",
"value": ["run", "start"]
}
]'
3. Restart and wait for rollout¶
kubectl -n cms rollout restart deployment/strapi
kubectl -n cms rollout status deployment/strapi
4. Verify Production Mode¶
kubectl -n cms exec deploy/strapi -- sh -lc 'echo NODE_ENV=$NODE_ENV; ps aux | grep strapi | head -n 10'
Expected output:
- NODE_ENV=production
- Process contains strapi start
Switch Strapi to Development Mode¶
⚠️ Development mode should be temporary.
1. Set NODE_ENV=development¶
kubectl -n cms set env deployment/strapi NODE_ENV=development
2. Run Strapi using npm run develop¶
kubectl -n cms patch deployment strapi --type='json' -p='[
{
"op": "replace",
"path": "/spec/template/spec/containers/0/command",
"value": ["npm"]
},
{
"op": "replace",
"path": "/spec/template/spec/containers/0/args",
"value": ["run", "develop", "--", "--watch-admin"]
}
]'
3. Restart and wait for rollout¶
kubectl -n cms rollout restart deployment/strapi
kubectl -n cms rollout status deployment/strapi
4. Verify Development Mode¶
kubectl -n cms exec deploy/strapi -- sh -lc 'echo NODE_ENV=$NODE_ENV; ps aux | grep strapi | head -n 10'
Expected output:
- NODE_ENV=development
- Process contains strapi develop --watch-admin
Common Issues & Notes¶
sh: 1: strapi: not found¶
Always run Strapi via npm scripts (npm run start / npm run develop).
Vite Error: Host Not Allowed¶
Occurs only in dev mode. Fix by adding src/admin/vite.config.ts and allowing the hostname, then rebuilding the image.
Summary¶
| Action | Production | Development |
|---|---|---|
NODE_ENV |
production | development |
| Command | npm run start |
npm run develop -- --watch-admin |
| Schema Editing | ❌ | ✅ |
16. TODO¶
- Backup PVC + DB
- Object storage (S3 / MinIO)
- Keycloak integration
- Rate limiting / WAF
- Horizontal scaling
Status: Production‑ready