Strapi
Published in

Strapi

How to Deploy and Scale Strapi on a Kubernetes Cluster 2/2

Discover how to integrate Strapi with Kubernetes through a comprehensive, step-by-step tutorial series. Explore the process from building your image to deploying a highly available and robust application.

Pre-requisites

Source Code

Advanced Deployment

Probes

App probes

# ~/strapi-k8s/helm/app.yaml
# ...
livenessProbe:
httpGet:
path: /
port: http

readinessProbe:
httpGet:
path: /
port: http
# from the helm folder
helm upgrade strapi strapi-chart -f app.yaml --atomic

DB probes

# ~/strapi-k8s/helm/db.yaml
# ...
livenessProbe:
exec:
command: ["bash", "-c", "mysqladmin --user=$MYSQL_USER --password=$MYSQL_PASSWORD ping"]
initialDelaySeconds: 30
timeoutSeconds: 5

readinessProbe:
exec:
command: ["bash", "-c", "mysql --host=127.0.0.1 --user=$MYSQL_USER --password=$MYSQL_PASSWORD -e 'SELECT 1'"]
initialDelaySeconds: 5
periodSeconds: 2
# from the helm folder
helm upgrade mysql strapi-chart -f db.yaml --atomic
# ~/strapi-k8s/helm/strapi-chart/templates/deployment.yaml
# ...
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
strategy:
type: {{ .Values.strategy | default "RollingUpdate" }}
selector:
matchLabels:
{{- include "strapi-chart.selectorLabels" . | nindent 6 }}
# ...
# ~/strapi-k8s/helm/strapi-chart/values.yaml
# ...
strategy: RollingUpdate
# ...
# ~/strapi-k8s/helm/db.yaml
# ...
strategy: Recreate
# from the helm folder
helm upgrade mysql strapi-chart -f db.yaml --atomic

Assets

NFS Server (temporary)

# create the rbac rules needed
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/nfs-ganesha-server-and-external-provisioner/master/deploy/kubernetes/rbac.yaml
# create the base pvc
kubectl apply -f https://raw.githubusercontent.com/kbristow/k3d-nfs-dynamic-volumes/main/pvc.yaml
# create the deployment
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/nfs-ganesha-server-and-external-provisioner/master/deploy/kubernetes/deployment.yaml
# patch the deployment to use the pvc instead of the default hostPath volume
kubectl patch deploy nfs-provisioner --type=json -p='[{"op": "replace", "path": "/spec/template/spec/volumes", "value": [{ "name": "export-volume", "persistentVolumeClaim": {"claimName": "ganesha-pvc"}}]}]'
# create the storage class
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/nfs-ganesha-server-and-external-provisioner/master/deploy/kubernetes/class.yaml

App conf

# ~/strapi-k8s/helm/app.yaml
# ...
storage:
claim:
enabled: true
capacity: 1Gi # it's not really relevant when using an NFS disk
accessModes:
- ReadWriteMany
storageClassName: example-nfs
mountPath: "/opt/app/public/uploads"
helm upgrade strapi strapi-chart -f app.yaml --atomic
ls -la /tmp/k3d

High Availability

Replicas

# ~/strapi-k8s/helm/app.yaml
replicaCount: 3
# ...
helm upgrade strapi strapi-chart -f app.yaml --atomic
kubectl get pods -l app.kubernetes.io/instance=strapi
# the output should be similar to this:
NAME READY STATUS RESTARTS AGE
strapi-strapi-chart-7679457c49-5qqnc 1/1 Running 0 21m
strapi-strapi-chart-7679457c49-h825x 1/1 Running 0 99s
strapi-strapi-chart-7679457c49-7tt7j 1/1 Running 0 99s
stern strapi-strapi-chart -n default

Affinity

# ~/strapi-k8s/helm/app.yaml
# ...
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/instance
operator: In
values:
- strapi
topologyKey: kubernetes.io/hostname
helm upgrade strapi strapi-chart -f app.yaml --atomic
kubectl get pods -l app.kubernetes.io/instance=strapi -o wide

(Bonus) Topology spread

topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/instance: strapi

Resources

# ~/strapi-k8s/helm/app.yaml
# ...
resources:
requests:
cpu: 100m
memory: 2Gi
limits:
cpu: 2000m
memory: 4Gi
helm upgrade strapi strapi-chart -f app.yaml --atomic

(Detour) Prometheus and Grafana

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install prometheus prometheus-community/kube-prometheus-stack --namespace prometheus --create-namespace --set grafana.service.type="NodePort" --set grafana.service.nodePort="30082"

Pod disruption budget (PDB)

# ~/strapi-k8s/helm/strapi-chart/templates/pdb.yaml

{{- $replicaInt := .Values.replicaCount | int }}
{{- if ge $replicaInt 2 }}
{{- if .Values.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "strapi-chart.fullname" . }}
labels:
{{- include "strapi-chart.labels" . | nindent 4 }}
spec:
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
selector:
matchLabels:
{{- include "strapi-chart.selectorLabels" . | nindent 6 }}
{{- end }}
{{- end }}
# ~/strapi-k8s/helm/strapi-chart/values.yaml
# ...
podDisruptionBudget:
enabled: true
maxUnavailable: 1
helm upgrade strapi strapi-chart -f app.yaml --atomic

Security

# ~/strapi-k8s/helm/app.yaml
# ...
securityContext:
runAsNonRoot: true
runAsUser: 2001
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
capabilities:
drop:
- ALL
helm upgrade strapi strapi-chart -f app.yaml --atomic
# ~/strapi-k8s/Dockerfile.prod
# ...
# ... RUN rm -rf /var/cache/apk/*
RUN addgroup --gid 2000 strapi \
&& adduser --disabled-password --gecos "" --no-create-home \
--uid 2001 --ingroup strapi strapi
USER strapi
# ... ARG NODE_ENV=production
# ...
docker build -t mystrapiapp-prod:0.0.2 -f Dockerfile.prod .
docker tag mystrapiapp-prod:0.0.2 localhost:5050/mystrapiapp-prod:0.0.2
docker push localhost:5050/mystrapiapp-prod:0.0.2
helm upgrade strapi strapi-chart -f app.yaml --atomic
kubectl logs --selector app.kubernetes.io/instance=strapi --tail=10 --follow
# the output should be similar to this:
warning Skipping preferred cache folder "/home/strapi/.cache/yarn" because it is not writable.
warning Skipping preferred cache folder "/tmp/.yarn-cache-2001" because it is not writable.
warning Skipping preferred cache folder "/tmp/.yarn-cache" because it is not writable.
# ~/strapi-k8s/helm/strapi-chart/values.yaml
# ...
volumes: {}
# ~/strapi-k8s/helm/app.yaml
# ...
volumes:
yarn-cache:
mount: /tmp
definition:
emptyDir: {}
# ~/strapi-k8s/helm/strapi-chart/templates/deployment.yaml
# ...
{{- if or .Values.storage.claim.enabled .Values.volumes }}
volumes:
{{- if .Values.storage.claim.enabled }}
- name: {{ include "strapi-chart.fullname" . }}-storage
persistentVolumeClaim:
claimName: {{ include "strapi-chart.fullname" . }}-pvc
{{- end }}
{{- range $key, $val := .Values.volumes }}
- name: {{ $key }}
{{- toYaml $val.definition | nindent 10 }}
{{- end }}
{{- end }}
# ~/strapi-k8s/helm/strapi-chart/templates/deployment.yaml
# ...
{{- if or .Values.storage.claim.enabled .Values.volumes }}
volumeMounts:
{{- if .Values.storage.claim.enabled }}
- name: {{ include "strapi-chart.fullname" . }}-storage
mountPath: {{ .Values.storage.mountPath }}
{{- end }}
{{- range $key, $val := .Values.volumes }}
- name: {{ $key }}
mountPath: {{ $val.mount }}
{{- end }}
{{- end }}
# ...
helm upgrade strapi strapi-chart -f app.yaml --atomic

Autoscaling

echo "GET http://localhost:1337/api/restaurants" | vegeta attack -duration=60s -rate=5 | vegeta report --type=text
# the output should be similar to this:
Requests [total, rate, throughput] 300, 5.02, 5.02
Duration [total, attack, wait] 59.807s, 59.801s, 6.298ms
Latencies [min, mean, 50, 90, 95, 99, max] 5.964ms, 9.857ms, 9.284ms, 13.749ms, 15.139ms, 20.418ms, 27.374ms
Bytes In [total, mean] 584700, 1949.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:300
Error Set:
echo "GET http://localhost:1337/api/restaurants" | vegeta attack -duration=60s -rate=30 | vegeta report --type=text
echo "GET http://localhost:1337/api/restaurants" | vegeta attack -duration=60s -rate=60 | vegeta report --type=text
echo "GET http://localhost:1337/api/restaurants" | vegeta attack -duration=60s -rate=100 | vegeta report --type=text
echo "GET http://localhost:1337/api/restaurants" | vegeta attack -duration=60s -rate=500 | vegeta report --type=text
# ~/strapi-k8s/helm/app.yaml
# ...
resources:
requests:
cpu: 300m
memory: 2Gi
limits:
cpu: 1000m
memory: 4Gi
helm upgrade strapi strapi-chart -f app.yaml --atomic
# ~/strapi-k8s/helm/app.yaml
# ...
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 100
targetCPUUtilizationPercentage: 80
helm upgrade strapi strapi-chart -f app.yaml --atomic

Custom metrics

Conclusions

--

--

Strapi is the leading open-source headless CMS. It’s 100% Javascript, fully customizable and developer-first. Unleash your content with Strapi.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store