DEV Community

Cover image for Deploying Strapi on a Kubernetes Cluster
Sanskriti Harmukh for Vultr

Posted on with Aashish Chaurasiya • Originally published at docs.vultr.com

Deploying Strapi on a Kubernetes Cluster

Strapi is a headless CMS with an intuitive admin dashboard, custom content types, and role-based permissions, well-suited to Kubernetes for its scalability and multi-environment support. This guide builds a Strapi app, containerizes it, and deploys it to Kubernetes behind an Nginx Ingress Controller with cert-manager TLS.

Prerequisites: a Kubernetes cluster with kubectl/helm configured, a PostgreSQL database, Docker on your workstation, a domain (this guide uses strapi.example.com) pointed at the ingress load balancer once provisioned.


Build the Strapi Application

1. Install Node.js:

$ curl -sL https://deb.nodesource.com/setup_22.x | sudo -E bash -
$ sudo apt install -y nodejs
Enter fullscreen mode Exit fullscreen mode

2. Configure Git if not already set up:

$ git config --global user.name "Your Full Name"
$ git config --global user.email "email@example.com"
Enter fullscreen mode Exit fullscreen mode

3. Scaffold a project:

$ npx create-strapi-app@latest strapi-project --quickstart --js
Enter fullscreen mode Exit fullscreen mode

Once it's running, press Ctrl+C to stop the dev server and continue configuring it.


Containerize the Application

1. Point it at PostgreSQL — edit .env in the project directory:

$ cd strapi-project
$ nano .env
Enter fullscreen mode Exit fullscreen mode
DATABASE_CLIENT=postgres
DATABASE_HOST=your-db-host
DATABASE_PORT=5432
DATABASE_NAME=strapi_db
DATABASE_USERNAME=user
DATABASE_PASSWORD=password
DATABASE_SSL=true
DATABASE_SSL_REJECT_UNAUTHORIZED=false
DATABASE_SCHEMA=public
DATABASE_CONNECTION_TIMEOUT=60000
Enter fullscreen mode Exit fullscreen mode

2. Replace config/server.js:

$ mv config/server.js config/server.ORIG
$ nano config/server.js
Enter fullscreen mode Exit fullscreen mode
module.exports = ({ env }) => ({
  host: env('HOST', '0.0.0.0'),
  port: env.int('PORT', 1337),
  url: env('STRAPI_URL'),
  app: {
    keys: env.array('APP_KEYS'),
  },
  webhooks: {
    populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false),
  },
});
Enter fullscreen mode Exit fullscreen mode

3. Set the URL, install the PostgreSQL driver, rebuild, run:

$ export STRAPI_URL="http://127.0.0.1:1337"
$ npm install pg --save
$ npm run build
$ npm run start &
Enter fullscreen mode Exit fullscreen mode

Visit http://<server-ip>:1337 and confirm the admin registration screen loads.

Build and Push the Docker Image

1. Dockerfile:

$ nano Dockerfile
Enter fullscreen mode Exit fullscreen mode
FROM node:22-alpine

RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev nasm bash vips-dev
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

WORKDIR /opt/
COPY package.json package-lock.json ./
RUN npm install
ENV PATH=/opt/node_modules/.bin:$PATH

WORKDIR /opt/app
COPY . .
RUN chown -R node:node /opt/app
USER node
RUN ["npm", "run", "build"]

EXPOSE 1337

CMD ["npm", "run", "start"]
Enter fullscreen mode Exit fullscreen mode

2. .dockerignore:

$ nano .dockerignore
Enter fullscreen mode Exit fullscreen mode
node_modules/
Enter fullscreen mode Exit fullscreen mode

3. Build, tag, push:

$ docker build -t strapi:latest .
$ docker login -u <username> -p <password>
$ docker tag strapi:latest <username>/strapi:latest
$ docker push <username>/strapi:latest
Enter fullscreen mode Exit fullscreen mode

Prepare the Cluster

Install the Nginx Ingress Controller

$ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
$ helm repo update
$ kubectl create namespace ingress-nginx
$ helm install ingress-nginx ingress-nginx/ingress-nginx --namespace ingress-nginx
Enter fullscreen mode Exit fullscreen mode

A LoadBalancer provisions automatically:

$ kubectl get services ingress-nginx-controller --namespace ingress-nginx
Enter fullscreen mode Exit fullscreen mode

It may take a few minutes for EXTERNAL-IP to populate.

Install cert-manager and a ClusterIssuer

$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.2/cert-manager.yaml
Enter fullscreen mode Exit fullscreen mode

Point your domain's A record at the Ingress controller's EXTERNAL-IP, then:

$ nano clusterissuer.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: "hello@example.com"
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            class: nginx
Enter fullscreen mode Exit fullscreen mode
$ kubectl apply -f clusterissuer.yaml
$ kubectl get clusterissuer letsencrypt-prod
Enter fullscreen mode Exit fullscreen mode

READY: True confirms it's live.


Deploy Strapi

1. Deployment (2 replicas):

$ nano strapi-deployment.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: apps/v1
kind: Deployment
metadata:
  name: strapi-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      name: strapi-app
  template:
    metadata:
      labels:
        name: strapi-app
    spec:
      imagePullSecrets:
        - name: regcred
      containers:
        - name: strapi
          image: <username>/strapi:latest
          imagePullPolicy: Always
          command: ["sh", "-c", "npm run build && npm run start"]
          ports:
            - containerPort: 1337
          env:
            - name: STRAPI_URL
              value: "strapi.example.com"
Enter fullscreen mode Exit fullscreen mode

regcred is a pull secret if your image lives in a private registry.

$ kubectl apply -f strapi-deployment.yaml
$ kubectl get deployment strapi-deployment
Enter fullscreen mode Exit fullscreen mode

2. Service:

$ nano strapi-service.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: v1
kind: Service
metadata:
  name: strapi-service
spec:
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 1337
  selector:
    name: strapi-app
Enter fullscreen mode Exit fullscreen mode
$ kubectl apply -f strapi-service.yaml
Enter fullscreen mode Exit fullscreen mode

3. Ingress with TLS:

$ nano strapi-ingress.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: strapi-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - secretName: strapi-tls
      hosts:
        - strapi.example.com
  rules:
    - host: strapi.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: strapi-service
                port:
                  number: 80
Enter fullscreen mode Exit fullscreen mode
$ kubectl apply -f strapi-ingress.yaml
$ kubectl get ingress
Enter fullscreen mode Exit fullscreen mode

Visit https://strapi.example.com — you should hit the login page over HTTPS.


Scale the Deployment

$ kubectl scale deployment/strapi-deployment --replicas=4
$ kubectl get deployment strapi-deployment
$ kubectl get pods
Enter fullscreen mode Exit fullscreen mode

Scale back down:

$ kubectl scale deployment/strapi-deployment --replicas=2
Enter fullscreen mode Exit fullscreen mode

Test fault tolerance by deleting a pod. Kubernetes replaces it and the Ingress routes around the gap with no downtime:

$ kubectl delete pod <pod-name>
Enter fullscreen mode Exit fullscreen mode

Next Steps

Strapi is running on Kubernetes with TLS termination and horizontal scaling. From here:

  • Add a HorizontalPodAutoscaler to scale on CPU/memory instead of manually
  • Move media uploads to S3-compatible object storage via a Strapi upload provider plugin
  • Set resource requests/limits on the Deployment to avoid noisy-neighbor scheduling issues

For the full guide, visit the original article on Vultr Docs.

Top comments (0)