Feature image for "Deploying Strapi applications to Kubernetes."

Strapi is the leading open-source headless content management system (CMS). It’s 100% JavaScript, fully customizable, and takes a developer-first approach. Strapi provides you with an interface to create and manage all the resources for your website. You can then build a front end to connect to your Strapi API with your favorite tools and frameworks. Content editors can use the friendly administration panel to manage and distribute content. Strapi is also based on a plugin system, which makes the CMS flexible and extensible.

Once you've built your resources with Strapi's administration panel and designed a nice front end to serve the content, you will need to deploy the application somewhere. This article shows you how to deploy a Strapi application on a Kubernetes or Red Hat OpenShift cluster.

Step 1: Set up the development environment

To use Strapi in a containerized development environment, you will need three independent containers: One to run the database, another for Strapi, and one for the front end. This section shows you how to set up the three containers you will use in development.

Initial setup

The database and back-end servers must be able to communicate. You can use a Docker network for this communication. Create your network with the following command:

$ docker network create strapi

You will also need three folders to hold the data from your containers. Here is the command to create the /data/app, and /front folders:

$ mkdir ./data && mkdir ./app && mkdir ./front

Create the database container

To start a Strapi instance, you will need a database to persist your data. In this example, we'll use a MySQL database server running inside a container. This way, there is no need to go through the process of installing MySQL.

To run the server, you can use the docker run command with the -d argument to run in the background. Include the following parameters:

  • --name to name the container.
  • -v to specify a folder to contain the data to reuse the next time you start the server.
  • -e to set up the environment variables to configure the database.

The command to start the container should look like this:

$ docker run --rm -d --name strapi-db -v $(pwd)/data:/var/lib/mysql:z --network=strapi -e MYSQL_DATABASE=strapi -e MYSQL_USER=strapi -e MYSQL_PASSWORD=strapi -e MYSQL_ROOT_PASSWORD=strapi-admin mysql:5.7

Note that we use the --network parameter to connect the database container to the network we created earlier.

After executing this command, try a docker ps to validate that the container has started.

Create the back-end container

Now that you've configured the database, you can start your strapi instance, which will run from a container. This time, you will use the strapi/strapi base image. You can still use the -d argument to run it in the background and --name to name your container. Be sure to also add the Strapi container to the same network as the database.

You should also map your local /app folder to /srv/app:

  • Use the -v parameter so that you can persist the files created by Strapi using a local folder on your machine.
  • Map a port on your operating system to access port 1337 inside the container. If you are using port 8080, the address to connect to the Strapi admin console will be localhost:8080.
  • Configure Strapi to use the database you started in the previous step using environment variables.

Here is the command to start the Strapi back-end container:

$ docker run --rm -d --name strapi-dev -p 8080:1337 -v $(pwd)/app:/srv/app:z --network=strapi -e DATABASE_CLIENT=mysql -e DATABASE_NAME=strapi -e DATABASE_HOST=strapi-db -e DATABASE_PORT=3306 -e DATABASE_USERNAME=strapi -e DATABASE_PASSWORD=strapi strapi/strapi

If Strapi can't find any files in the local file system that you mapped, it will automatically create a new instance of a Strapi server. This can take a few minutes. You can use docker logs to keep an eye on the application creation status:

$ docker logs -f strapi-dev

If you want to stop the logs in your console, enter Ctrl-C.

Once you see a message stating that your Strapi server is started, you can go to http://localhost:8080/admin to create your admin user.

After you've created the admin user, go ahead and create a new content type and make it publicly available. For content to work with in the next step, create a Content-Type for Posts. It will have four fields: titleauthor (a relationship to Users), publish_date, and content, as shown in Figure 1.

The Posts content type has four fields: Title, Author, Publish_date, and Content.
Figure 1: The new content type with four fields for posts.

Note: See this video from the Strapi team for a full tutorial about creating new content types.

Create the front-end container

Next up, you will create a front end. This user interface (UI) will consist of a simple HTML file that fetches the Strapi application programming interface (API) data and displays it on the page.

We'll use a Nginx server to display the content. You can start the container similarly to how you started the other two. This time, map port 80 in the container to port 8888 on your local machine and mount the /front folder to map to /usr/share/nginx/html inside your container. The /front folder is the default folder to serve files from with Nginx:

$ docker run --rm -d --name strapi-front -p 8888:80 -v $(pwd)/front:/usr/share/nginx/html:z nginx:1.17

Now, go ahead and create your front-end application. You might use a React, VueJS, or Angular application, but we'll use a simple HTML file for this demo. The file will do a fetch from the Strapi API to download the data and then create the necessary elements on the page using JavaScript.

The HTML page will have a single div where the JavaScript code appends the API's content. Create the following index.html file in the /front folder:

<body>
  <div id="content"></div>
</body>

You will need to add a script tag to include a configuration file, which will make it easier to overwrite your Strapi API location later. Add the following inside the index.html:

<script type="text/javascript" src="config.js">

The front/config.js file should create a global constant with the following configuration:

const config = {
  BASE_URL: "http://localhost:8080"
}

Finally, in the index.html file, add another script tag that contains the code to download the content and display it on the page:

window.addEventListener("DOMContentLoaded", e => {
  console.log("Loading content from Strapi");

  const BASE_URL = config.BASE_URL;

  const BLOG_POSTS_URL = `${BASE_URL}/posts`;

  fetch(BLOG_POSTS_URL).then(resp => resp.json()).then(posts => {
    for(let i = 0; i < posts.length; i++) {
      let postData = posts[i];
      let post = document.createElement("div");
      let title = document.createElement("h2");
      title.innerText = postData.title;
      let author = document.createElement("h3");
      author.innerText = `${postData.author.firstname} ${postData.author.lastname} -- ${postData.publish_date}`;
      let content = document.createElement("div");
      content.innerText = postData.content;
      post.appendChild(title);
      post.appendChild(author);
      post.appendChild(content);
      document.querySelector("#content").appendChild(post);
    }
  });
});

Now that you've created all the files go to http://localhost:8888 to see your application. You should see your fancy UI serving content from Strapi.

Step 2: Set up the production environment

When you are ready to deploy your application, you will need to create your own containers that contain all the necessary files and data. These containers will go live on the web.

For each container, you will need to create a Dockerfile. You will use the Dockerfiles to create your containers with the actual content. Then, you'll deploy the containers to Kubernetes or OpenShift.

Create the database container

There is a good chance that you already have a database in production, and you probably won't want to overwrite its contents. For this reason, you will use the same default MySQL image that you used in development for the production database. If you want to import the SQL content later, you can use Docker to run a mysqldump command on your database:

$ docker exec strapi-db /bin/bash -c 'mysqldump strapi -ustrapi -pstrapi' | tee strapi-db.sql

This file will be imported into the production database later if it's needed.

Note: The mysqldump command uses tee to copy the contents to a file. If you don't have the tee command, you can copy the docker command's output into a file named strapi-db.sql.

Create the back-end container

Next, you will create a Dockefile.back to build your container for the back end.

Start from the strapi base image FROM strapi/base. Change the working directory to /opt/app and copy all the local files into the container. Next, expose port 1337 and set all your environment variables. Don't forget to add an environment variable for NODE_ENV=production. Finally, execute yarn build to build all the production resources and use the CMD command to start the back end once the container is started.

Note: For more about using the Strapi base image, see the Strapi documentation on GitHub.

FROM strapi/base
WORKDIR /opt/app
COPY ./app/package.json ./
COPY ./app/yarn.lock ./
RUN yarn install
COPY ./app .
ENV NODE_ENV production
ENV DATABASE_CLIENT=mysql
ENV DATABASE_NAME=strapi
ENV DATABASE_HOST=strapi-db
ENV DATABASE_PORT=3306
ENV DATABASE_USERNAME=strapi
ENV DATABASE_PASSWORD=strapi
RUN yarn build
EXPOSE 1337
CMD ["yarn", "start"]

Create the front-end container

You'll have to do a bit of bash scripting to use an environment variable to specify your Strapi server's URL.

Note: See my best practices for JavaScript front-end containers for more about using environment variables with front-end containers.

First, start with the nginx:1.17 base image and change the working directory to /usr/share/nginx/html. In there, copy all the files from your local system into the container.

The next step involves using sed to change the BASE_URL value to $BASE_URL. Then, you will pipe in the result to a new file called config.new.js and rename the file to config.js, overwriting the original.

The result inside the container is a new config.js file that looks like the one below. Note that the original file in your local file system is left intact:

const config = {
  BASE_URL: "$BASE_URL"
}

Finally, you will need to use envsubst to change the value of $BASE_URL to the environment variable's actual value. Make the following updates in the ENTRYPOINT, so the changes will happen when someone issues a Docker run:

  • Use a cat command to pipe the config.js file into envsubst.
  • Pipe the output to tee to create a new config.new.js file and rename the file to overwrite the previous one.
  • Use thenginx -g 'daemon off;' command to start the Nginx server:
    FROM nginx:1.17
    WORKDIR /usr/share/nginx/html
    COPY ./front/*.* ./
    RUN sed s/BASE_URL\:\ \"[a-zA-Z0-9:\/]*\"/BASE_URL\:\ \"\$BASE_URL\"/g config.js > config.new.js && mv config.new.js config.js
    ENTRYPOINT cat config.js |  envsubst | tee config.new.js && mv config.new.js config.js && nginx -g 'daemon off;'
    

Updating the entry point instead of a RUN lets you specify different values for the base URL according to where the container is running.

Build the containers

Now that you have all your Dockerfiles ready, you can build the containers and push them to your favorite image registry. Don't forget to change the name of your images to use your username for that registry:

$ docker build -t $DOCKER_USERNAME/strapi-front -f Dockerfile.front .
$ docker build -t $DOCKER_USERNAME/strapi-back -f Dockerfile.back .
$ docker push $DOCKER_USERNAME/strapi-front
$ docker push $DOCKER_USERNAME/strapi-back

Step 3: Package and run the application

Now that you have containers with all of your code and data, you are ready to deploy the containers somewhere. We'll use Docker and Docker Compose to run the application and a Kubernetes or OpenShift cluster to deploy it.

Package and run the application with Docker

If you want to run this application, you can start all the containers in the same way you would in production.

The commands to start the containers are similar to those you used in development mode, but with the mounted volumes and without the environment variables. We handled the source code and environment variables in the Dockerfile. Note that we add an environment variable specifying the location of the Strapi API for starting the front end:

$ docker run --rm -d --name strapi-db -v $(pwd)/data:/var/lib/mysql:z --network=strapi -e MYSQL_DATABASE=strapi -e MYSQL_USER=strapi -e MYSQL_PASSWORD=strapi -e MYSQL_ROOT_PASSWORD=strapi-admin mysql:5.7
$ docker run --rm -d --name strapi -p 1337:1337 --network=strapi $DOCKER_USERNAME/strapi-back
$ docker run --rm -d --name strapi-front -p 8080:80 -e BASE_URL=http://localhost:1337 $DOCKER_USERNAME/strapi-front

Package and run the application with Docker Compose

If you want to share your application code and configurations with anyone else, you could provide them with a docker-compose.yaml file. This tool lets you manage multiple containers at once without multiple bash commands:

version: '3'
services:
  strapi-db:
    image: mysql:5.7
    volumes:
      - ./data:/var/lib/mysql
    networks:
      - strapi
  strapi-back:
    image: $DOCKER_USERNAME/strapi-back
    ports:
      - '1337:1337'
    networks:
      - strapi
  strapi-front:
    image: $DOCKER_USERNAME/strapi-front
    ports:
      - '8080:80'
    environment:
      BASE_URL: http://localhost:1337
networks:
  strapi:

Step 4: Deploy the application

Once you've created all of your containers, you can deploy the application into a Kubernetes or OpenShift cluster. I'll show you how to do both.

Deploy the application on Kubernetes

Before deploying your application in a Kubernetes cluster, you will need to use YAML files to create all the necessary assets. For more details on each of these assets, see Kubernetes by example. To test out the deployment, you can use a smaller version of Kubernetes to run locally on your own machine. I've used Minikube for the following examples.

Deploying the database

The setup for persistent volumes (PVs) and persistent volume claims (PVCs) varies from one cloud provider to another. For this reason, the database in this example will not persist data. For more information about how to persist data, check your cloud provider's documentation.

For the database, we will need to create a deployment. You will start by creating a YAML file that describes your deployment. You can give it a name, and in the spec, you will create a template for the pods. Each pod will have a single container, which will be the ones that you've pushed to your registry. Here is the deployment for this example (deploy-db.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: strapi-db
spec:
  selector:
    matchLabels:
      component: db
  template:
    metadata:
      labels:
        component: db
    spec:
      containers:
      - name: strapi-db
        image: mysql:5.7
        env:
          - name: MYSQL_DATABASE
            value: strapi
          - name: MYSQL_USER
            value: strapi
          - name: MYSQL_PASSWORD
            value: strapi
          - name: MYSQL_ROOT_PASSWORD
            value: strapi-admin

Once you have your file, you can apply it to your cluster using kubectl:

$ kubectl apply -f ./deploy-db.yaml

Deploying the back end

Your back end needs to be able to find the pods inside the cluster, so you will need to create a Service to expose each pod. We are using the defaults here, so you can use kubectl to create this service:

$ kubectl expose deployment strapi-db --port 3306

If you want to import data from your development environment SQL, you can run the following commands:

$ kubectl cp ./strapi-db.sql $(kubectl get pod -l component=db | awk 'NR>1 {print $1}'):/tmp/strapi-db.sql
$ kubectl exec -t $(kubectl get pod -l component=db | awk 'NR>1 {print $1}') -- /bin/bash -c 'mysql strapi -ustrapi -pstrapi < /tmp/strapi-db.sql'

These commands copy the SQL file to the pods, then run a MySQL command to run it in the database.

You can also create your deployments for the back- and front-end portions of your application. The Strapi back end (deploy-back.yaml) is the same as the database deployment, apart from the name, label, and container image:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: strapi-back
spec:
  selector:
    matchLabels:
      app: strapi
      component: back
  template:
    metadata:
      labels:
        app: strapi
        component: back
    spec:
      containers:
      - name: strapi-back
        image: joellord/strapi-back

Deploying the front end

The front end (deploy-front.yaml) uses a similar structure to the back end, but you also need to set the environment variable for the back end's BASE_URL. For now, just set that variable's value to /api. You also need to expose the container to port 80 so that it will be available to the outside world eventually:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: strapi-front
spec:
  selector:
    matchLabels:
      component: front
  template:
    metadata:
      labels:
        component: front
    spec:
      containers:
      - name: front
        image: joellord/strapi-front
        ports:
          - containerPort: 80
        env:
          - name: BASE_URL
            value: /api

Create and expose the application services in your cluster

Now that you've created your deployment files, you can apply them to your cluster and create the services for each one:

$ kubectl apply -f ./deploy-back.yaml
$ kubectl apply -f ./deploy-front.yaml
$ kubectl expose deployment strapi-back --port 1337
$ kubectl expose deployment strapi-front --port 80

Everything is now running inside your cluster. You only need to expose the front- and back-end services to the outside world. For this, you will use an ingress.

Here, you will create an ingress that exposes the front end as the default service. Any incoming request to your cluster then goes to the front end by default. You will also add a rule that redirects any traffic sent to  /api/* to the back-end service. The request will be rewritten when it's sent to that service to remove the /api part of the URL. We'll add a Nginx annotation in the metadata to effect this change. Here is the ingress.yaml file:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: main-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  rules:
  - http:
      paths:
        - path: /api(/|$)(.*)
          pathType: Prefix
          back end:
            service:
              name: strapi-back
              port:
                number: 1337
        - path: /()(.*)
          pathType: Prefix
          backend:
            service:
              name: strapi-front
              port:
                number: 80

Go ahead and apply this file to your cluster. You might need to enable the following add-on if you are using Minikube and have never used an ingress before:

# For minikube users
$ minikube addons enable ingress

$ kubectl apply -f ./ingress.yaml

You now have everything needed to run your Strapi application in a Kubernetes cluster. Point your browser to the cluster URL, and you should see the full application running in your cluster. If you're using Minikube, you can use the command minikube ip to get your cluster's address.

Deploy the application on OpenShift

Deploying the application on OpenShift can be even easier than deploying in a Kubernetes cluster.

In this case, you can test out your deployment with the Developer Sandbox, which gives you access to an OpenShift cluster for free for 14 days.

Create the deployment from an image

The command-line interface (CLI) tool that you use to manage your cluster (oc) can create a deployment directly from an image. To deploy your application, enter:

$ oc new-app mysql:5.7 MYSQL_USER=strapi MYSQL_PASSWORD=strapi MYSQL_DATABASE=strapi -l component=db --name strapi-db
$ oc new-app joellord/strapi-back-openshift --name strapi-back
$ oc new-app joellord/strapi-front-openshift --name strapi-front

Note: Images on OpenShift need to be run as a non-root user. See my guide to front-end best practices for more about non-root images. The Dockerfiles used for this project can be found in the Git repository for this article under Dockerfile.rootless.back and Dockerfile.rootless.front.

Seed your database with the data that you exported earlier. This data should be in your current working directory and have the name strapi-db.sql.
$ oc exec -it $(oc get pods -l component=db | awk 'NR>1 {print $1}') -c strapi-db -- bash -c 'mysql -ustrapi -pstrapi strapi' < ./strapi-db.sql

Expose the application

Next, you'll want to expose the application to the outside world. OpenShift has a neat object for this purpose, Route, which you can use from the OpenShift CLI. Use the oc expose command to expose the back- and front-end to the outside world:

$ oc expose service strapi-back
$ oc expose service strapi-front --port=8080

Now that your back end is exposed, you will need to set your front-end environment variable to the back-end route. Start by getting the public route for the Strapi API:

$ oc get routes

You should see all the routes that you've created so far. You can store the back-end route in a variable and then set it as an environment variable using oc set env:

$ export BACKEND_ROUTE=$(oc get routes | grep strapi-back | awk '{print $2}')
$ oc set env deployment/strapi-front BASE_URL=http://$BACKEND_ROUTE

You can now access your Strapi application using the route for the strapi-front service.

Summary

When you are ready to put your Strapi application in production, the first step will be to containerize your whole setup. Once you have that done, you can deploy those containers in Kubernetes. You've also seen how easy it is to deploy a Strapi application to OpenShift.

Last updated: May 31, 2023