✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

Tutorials8 min read

Deploy Strapi on Kubernetes with HTTPS

July 2, 2020Updated on May 22, 2026

Deploy Strapi on Kubernetes with HTTPS

Disclaimer This article was written by Rajat Badjatya, a member of the Write for the community program

What is Strapi?

Strapi is a headless Content Management System. What does headless CMS mean? A headless CMS is a back-end only content management system (CMS) that makes content accessible via a RESTful API for display on any device.

Why Headless CMS?

An omnichannel experience of a product is the need of the hour. To deliver a seamless experience, you need an API-first headless CMS that can deliver content to multiple channels(Websites, Android Apps, iOS Apps, etc). To learn more about Strapi and headless CMS visit strapi.io

Though Strapi installation is quite easy on Linux and Windows host machine. But as all you know, Kubernetes adds a layer of abstraction which makes Strapi deployment somewhat effortful job. Also, Strapi doesn’t support HTTPs/SSL communication out of the box . So in this tutorial I will step by step walk you through the process of setting up and deploying production grade Strapi with HTTPs on Kubernetes.

As they say, let’s get started without any further delay.

Setup

Prerequisites: A basic knowledge of Kubernetes along with access to a Kubernetes cluster. I recommend Google Kubernetes Engine (GKE), but the majority of this tutorial can be used on any Kubernetes cluster. In a pinch, you can also use minikube so you have a zero-cost way to follow along with me.

I have divided the whole deployment process into 4 steps to make it more understandable

Steps:

  1. Setting up the namespace and storage-class.
  2. Installing MongoDB for our Strapi project.
  3. Creating a docker image for Strapi with MongoDB and HTTPs configuration.
  4. Creating and deploying Kubernetes resources for Strapi.

Step 1: Setting up the namespace and storage-class

Namespace:

By separating different projects in different namespaces, you can easily deploy as many Strapi projects. All you need to do is use a different namespace, and the new definitions will not overlap with the existing ones.

If you choose to use the default namespace, then you can skip this step. Otherwise, you should apply the namespace file given below:

namespace.yml

---
apiVersion: v1
kind: Namespace
metadata:
  name: ns-strapi
  labels:
    name: ns-strapi
$ kubectl apply -f namespace.yml
namespace/my-es-cluster created
$ kubectl get namespaces
NAME            STATUS    AGE
default         Active    50m
kube-system     Active    50m
ns-strapi       Active    1m

StorageClass

The next step is to define a new StorageClass for providing the persistent volumes for MongoDB and Strapi.

Each StorageClass contains the fields provisioner, parametersand reclaimPolicy, which are used when a PersistentVolume belonging to the class needs to be dynamically provisioned. If you are using the GCP then volume provisioner would be gce-pd. Alternatively, you can look for a more suitable option for StorageClass here.

sc-strapi.yml

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: sc-strapi
provisioner: kubernetes.io/gce-pd
reclaimPolicy: Retain
parameters:
  type: pd-ssd

Step 2: Installing MongoDB for our Strapi project

Strapi gives you the option to choose the most appropriate database for your project. It currently supports PostgreSQL, MongoDB, SQLite, MySQL, and MariaDB.

We will be setting up MongoDB for this tutorial and will be deploying it as a special Kubernetes resource known as StatefulSet instead of normal Deployment and the rationale behind that is “Containers are stateless!” they say, and “databases are pointless without state!”.

We want the volume with the mongo data to get attached again to a new Pod when the old Pod vanishes. If we don’t do this, we will lose all the data in the database after any of Pod goes down. The StatefulSet controller will give the replacement Pod the same name and access to the resources of the Pod it replaced.

If you want to learn more about MongoDB installation on Kubernetes you can refer to this article from Kubernetes Team or if you are in a hurry then just replace the name of your StorageClass and name of mongo service in below YAML to make your MongoDB pods up and running.

mongo.yml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: strapi-mongo
  namespace: ns-strapi
spec:
  serviceName: strapi-mongo-svc
  replicas: 3
  selector:
    matchLabels:
      app: strapi-mongo
  template:
    metadata:
      labels:
        app: strapi-mongo
        selector: strapi-mongo
    spec:
      affinity:
# Try to put each mongo pod on a different node in the K8s cluster
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - strapi-mongo
                topologyKey: kubernetes.io/hostname
      initContainers:
 # Fix the permissions on the volume.      
        - name: fix-the-volume-permission
          image: busybox
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
             cpu: 100m
             memory: 256Mi
          command: ['sh', '-c', 'chown -R 1000:1000 /data/db']
          securityContext:
            privileged: true
          volumeMounts:
           - name: mongo-volume
             mountPath: /data/db
      containers:
        - name: strapi-mongo
          image: mongo:4.0.8
          ports:
            - containerPort: 27017
              name: tcp
          securityContext:
            runAsUser: 1000
            runAsGroup: 1000
          resources:
            limits:
              cpu: 500m
              memory: "1Gi"
            requests:
              cpu: 500m
              memory: "1Gi"
          volumeMounts:
            - name: mongo-volume
              mountPath: /data/db
  volumeClaimTemplates:
    - metadata:
        name: mongo-volume
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: sc-strapi
        volumeMode: Filesystem
        resources:
          requests:
            storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: strapi
  name: mongo
  namespace: ns-strapi
spec:
  type: LoadBalancer
  ports:
  - name: mongo
    port: 30144
    protocol: TCP
    targetPort: 27017
  selector:
    app: strapi-mongo

The above YAML is pretty much the same as the YAML Kubernetes Team is using for MongoDB installation at this link. I just did 2 modifications, First, I added the pod’s affinity & anti-affinity to ensure the high availability of mongo pods these properties tell Kubernetes to schedule our pods to a different-different node that could be helpful in a case when a node gets crashed. Second, I added a security context to avoid permissions error and seamlessly access path data/db inside the container.

If you are following me then after applying the above YAML, you should have three pods with a service created in your cluster. These correspond to the three nodes in your MongoDB set. You can see them with this command:

kubectl get pods -n ns-strapi

NAME   READY STATUS RESTARTS AGE

strapi-mongo-0 1/1  Running 0     3m

strapi-mongo-1 1/1  Running 0     3m

strapi-mongo-2 1/1  Running 0     3m**kubectl get svc -n ns-strapi**NAME     TYPE           CLUSTER-IP      EXTERNAL-IP   PORT
mongo    LoadBalancer   10.97.135.233   xxx.xx.xx.xx 30144:30129/TCP

Step 3: Setting up Strapi with MongoDB configuration.

Now comes the critical step of setting up the Strapi, let’s start by creating a docker file.

  • Creating a docker file

To have more control in our hand I am creating a Strapi image from scratch instead of using community image.

dockerfile

FROM node:11.1.0-alpine

WORKDIR /usr/src/api

RUN echo "unsafe-perm = true" >> ~/.npmrc

# Installing Strapi
RUN npm install -g strapi@3.0.0-beta.18

# Installing nginx
RUN  apk add nginx
RUN mkdir -p /etc/nginx/certs

# Copying the certificates and nginx config
COPY domain.crt  /etc/nginx/certs/domain.crt
COPY domain.key  /etc/nginx/certs/domain.key
COPY nginx.conf /etc/nginx/nginx.conf

# Moving the start-up script to the appropriate location
COPY strapi.sh /root

# Giving read and execution permissions
RUN chmod +x /root/strapi.sh

WORKDIR /etc/init.d/

RUN mkdir -p /run/nginx
EXPOSE 1337

ENTRYPOINT [ "/root/strapi.sh" ]

Most of the statements in above docker file are self-explanatory but I want to bring your attention to a few things:

  1. I have chosen node:11.1.0-alpine as a base image for our app.
  2. I have installed Nginx in our container and kept SSL certs to a path /etc/nginx/certs/ and nginx.conf to the path /etc/nginx/nginx.conf
  3. Gave read and execution permission to the startup script which we’ll be creating in the immediate next step.
  • Creating a Startup script for Strapi image

We’ll use the shell script as the ENTRYPOINT of a container and execute the necessary steps in order to set up the environment for our Strapi project.

strapi.sh

#!/bin/sh

strapiCount=1
count=1
####################START_SERVICES#################################################
while :
do
sleep 5
namespace=${NAMESPACE}
APP_NAME=${APP_NAME}
DATABASE_CLIENT=${DATABASE_CLIENT}
# Kubenrnetes DNS URL for your mongo service: my-svc.my-namespace.svc.cluster-domain.example
DATABASE_HOST=${DATABASE_HOST}
DATABASE_PORT=${DATABASE_PORT}
DATABASE_NAME=${DATABASE_NAME}

if [ ! -f "/persistent/$namespace/${APP_NAME}/package.json" ] && [ ${strapiCount} == 1 ]
then
echo "====> creating project for strapi <===="
mkdir -p /persistent/$namespace/

cd /persistent/$namespace
strapi new ${APP_NAME} --dbclient=${DATABASE_CLIENT} --dbhost=${DATABASE_HOST} --dbport=${DATABASE_PORT} --dbname=${DATABASE_NAME} --dbusername=${DATABASE_USERNAME}   --dbpassword=${DATABASE_PASSWORD}

chown -R  /persistent/$namespace/${APP_NAME}
elif [ ! -d "/persistent/$namespace/${APP_NAME}/node_modules" ]
then
  echo "===> installing node modules in existing strapi project <====="
  npm install --prefix ./$APP_NAME
fi
if [ ${strapiCount} == 1 ]
then
echo "====> starting strapi <===="
cd /persistent/$namespace/${APP_NAME}
npm run build
strapi start &
fi
strapiCount=`expr $strapiCount + 1`
apk add openrc
nginx -g "daemon off;"
cd
sleep 60
done
else
   echo "not valid"
fi

while :
do
sleep 50
done

With the help of the above script, we are not only configuring the MongoDB required for Strapi but we are also handling the scenario in which our K8s pod got terminated and we want to start our Strapi project from the same stage where it was earlier. Also, we are kicking off Nginx using nginx -g “daemon off;

  • Nginx reverse proxy setting for redirecting HTTPs requests to HTTP

If you want to expose Strapi over HTTPs you need to apply some hack as native SSL support from Strapi was removed. A simple way to do so is by using the Nginx reverse proxy to listen to HTTPs traffic and redirect to Strapi running over HTTP.

nginx.conf

worker_processes 1;

events { worker_connections 2048; }

http {

    sendfile on;
    ssl_certificate     /etc/nginx/certs/domain.crt;
    ssl_certificate_key /etc/nginx/certs/domain.key;
    ssl_ciphers         EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH;
    ssl_protocols       TLSv1.1 TLSv1.2;

# Strapi upstream server
    server {

        client_body_buffer_size     32k;
        client_header_buffer_size   8k;
        large_client_header_buffers 8 64k;
        listen              443 ssl;
        listen              [::]:443 ssl;


         location / {
            proxy_pass         http://localhost:1337;
            proxy_redirect     off;
            proxy_set_header   Host $host;
            proxy_set_header   X-Real-IP $remote_addr;
            proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Host $server_name;
        }
    }

}

If you want to learn more about Nginx reverse proxy you can refer to this link.

Now we have dockerfile and startup script strapi.sh ready, let’s create the docker image and push it to the Docker repository.

Build and push image:

sudo docker build -f dockerfile -t YOUR\_REGISTRY:5000/strapi-image:v1 .sudo docker push YOUR\_REGISTRY:5000/strapi-image:v1

Step 4: Creating and deploying Kubernetes resources for Strapi

If you played your cards right so far you could have a namespace, storage classand animage ready. If that’s the case, then you are only 2 steps away to make your Strapi project up and running on Kubernetes.

First, let’s use the sc-strapi a storage class that we created in step-1 and create aPVC which will help us to connect to the Persistent Volume which will in turn connect to the underlying storage where we want to keep our Strapi project’s data.

kubectl apply -f pvc.yml -n ns-strapi

pvc.yml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-sc-strapi
  namespace: ns-strapi
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1G
  storageClassName: sc-strapi

Specify the newly created PVC name in the below YAML. You might also need to change the value of these variablesDATABASE_CLIENT, DATABASE_HOST, DATABASE_PORT, DATABASE_NAME in the below YAML according to your setup.

strapi.yml

kind: Service
metadata:
  name: strapi
  labels:
    app: strapi
spec:
  type: LoadBalancer
  ports:
  - name: strapi
    targetPort: 443
    protocol: TCP
    port: 30143
  selector:
    app: strapi
apiVersion: v1

---

kind: Deployment
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: strapi
    spec:
      containers:
      - image: YOUR_REGISTRY:5000/strapi-image:v
        imagePullPolicy: Always
        env:
        - name: NAMESPACE
          value: "ns-strapi"
        - name: "DATABASE_CLIENT"
          value: "mongo"
        - name: "DATABASE_NAME"
          value: "strapi-mongo"
        - name: "DATABASE_PORT"
          value: "30144"
        - name: "DATABASE_HOST"
          value: "mongo.ns-strapi.svc.cluster.local"
        - name: "APP_NAME"
          value: "strapi-app"
        name: masterstrapi
        ports:
        - containerPort: 1337
        resources:
          requests:
            cpu: '2'
            memory: 4Gi
          limits:
            cpu: '2'
            memory: 4Gi
        volumeMounts:
        - name: strapi-volume
          mountPath: "/persistent"
      volumes:
      - name: strapi-volume
        persistentVolumeClaim:
          claimName: pvc-sc-strapi
apiVersion: extensions/v1beta1
metadata:
  name: masterstrapi

After applying the above YAML you will see new pod(s) and service added in your namespace.

kubectl get pods -n ns-strapi

NAME              READY STATUS RESTARTS AGE

strapi-mongo-0     1/1  Running 0       20m

strapi-mongo-1     1/1  Running 0       20m

strapi-mongo-2     1/1  Running 0       20m**masterstrapi-6c5bs** **1/1  Running 0        3m****kubectl get svc -n ns-strapi**NAME     TYPE           CLUSTER-IP      EXTERNAL-IP   PORT
mongo    LoadBalancer   xx.xx.xx.xx     xxx.xx.xx.xx 30144:30129/TCP
**strapi   LoadBalancer   xx.xx.xx.xx     xxx.xx.xx.xx 30143:31745/TCP**

You can access the Strapi from your browser once your cloud provider controller configures the network load balancer and generate a stable IP address for Strapi service.

Note: Two or three links including open the administration button in Strapi UI panel will redirect you tohttp://localhost:1337/ which is happening because inside the container Strapi is still running on protocol HTTP and default port 1337. To access those links you need to copy the route and append it to your Strapi’s service load balancer IP.

To access the admin panel in the browser just copy the load balancer IP for Strapi service and append the route /admin to it.

Strapi GIF

Strapi accessible over browser

And, it brings us to the end of this article. I hope this article has enabled you to successfully deploy Strapi over HTTPs on Kubernetes. Feel free to comment out your thoughts and let me know if you face any issues in the implementation. Thank you Nitesh Ratnaparkhe and @Shubham Khatri for taking the time out to proofread this article and providing constructive feedback.

Rajat BadjatyaSenior Front-end | DevOps Engineer @ Tata Consultancy Services

Rajat is a creative frontend developer with 5+ years of experience in developing robust, scalable, and high-performance Single Page Apps and Progressive Web Apps and an active member of the Strapi community.

Streaming Strapi Dynamic Zones in Next.js 16: Part 2
Tutorials·14 min read

Streaming Strapi Dynamic Zones in Next.js 16: Part 2

Build the streaming UI for Strapi Dynamic Zones in Next.js 16: per-block Suspense boundaries, a block registry, webhook revalidation, and draft preview.

·July 16, 2026
Streaming Strapi Dynamic Zones in Next.js 16: Part 1
Tutorials·18 min read

Streaming Strapi Dynamic Zones in Next.js 16: Part 1

Learn how to model a Strapi 5 Dynamic Zone and build a typed two-phase fetch layer for Next.js 16 that streams content without client-side waterfalls.

·July 13, 2026
Building Docs for the AI Era, Part 1
Tutorials·10 min read

Building Docs for the AI Era, Part 1: Self-Healing Docs

Learn how Strapi uses AI, GitHub Actions, and progressive cost gating to automatically detect documentation gaps and keep docs aligned with code changes.

·June 11, 2026