Strapi: OS CMS

Published: Oct 31, 2023 by Isaac Johnson

Strapi is a CMS that has been around since 2016. They have both a self hosted and Cloud option. But more importantly, they are based on Open-Source and free to setup and run yourself, should you want to take the plunge.

Today we’ll start with a basic docker setup then the NodeJS version. We’ll work through Kubernetes and lastly sort out a self-built and hosted version with TLS ingress and PSQL backend.

Starting with Docker

We’ll run with docker (note, I found I needed to invoke twice in a row)

builder@LuiGi17:~$ docker run -it -p 1337:1337 -v `pwd`/project-name:/srv/app strapi/strapi
Node modules not installed. Installing...
npm WARN deprecated strapi-plugin-i18n@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-connector-bookshelf@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-utils@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-plugin-upload@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-plugin-content-type-builder@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-plugin-users-permissions@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-plugin-content-manager@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-plugin-email@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-admin@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
npm WARN deprecated strapi-helper-plugin@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-provider-upload-local@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-generate-api@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-generate@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
npm WARN deprecated boom@7.3.0: This module has moved and is now available at @hapi/boom. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.
npm WARN deprecated koa-router@7.4.0: **IMPORTANT 10x+ PERFORMANCE UPGRADE**: Please upgrade to v12.0.1+ as we have fixed an issue with debuglog causing 10x slower router benchmark performance, see https://github.com/koajs/router/pull/173
npm WARN deprecated strapi-generate-model@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-generate-controller@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-generate-service@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-generate-policy@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-provider-email-sendmail@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-database@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-generate-plugin@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated strapi-generate-new@3.6.8: Strapi V3 is no longer maintained
npm WARN deprecated @babel/plugin-proposal-class-properties@7.18.6: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.
npm WARN deprecated @babel/plugin-proposal-async-generator-functions@7.20.7: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.
npm WARN deprecated @babel/polyfill@7.12.1: 🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information.
npm WARN deprecated html-webpack-plugin@3.2.0: 3.x is no longer supported
npm WARN deprecated node-pre-gyp@0.11.0: Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future
npm WARN deprecated @formatjs/intl-utils@2.3.0: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
npm WARN deprecated intl-messageformat-parser@5.5.1: We've written a new parser that's 6x faster and is backwards compatible. Please use @formatjs/icu-messageformat-parser
npm WARN deprecated har-validator@5.1.5: this library is no longer supported
npm WARN deprecated hoek@6.1.3: This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.
npm WARN deprecated formidable@1.2.6: Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau
npm WARN deprecated sequelize@5.22.5: Please update to v6 or higher! A migration guide can be found here: https://sequelize.org/v6/manual/upgrade-to-v6.html
npm WARN deprecated @formatjs/intl-unified-numberformat@3.3.7: We have renamed the package to @formatjs/intl-numberformat
npm WARN deprecated chokidar@2.1.8: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies
npm WARN deprecated core-js@2.6.12: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
npm WARN deprecated tar@2.2.2: This version of tar is no longer supported, and will not receive security updates. Please upgrade asap.
npm WARN deprecated popper.js@1.16.1: You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1
npm WARN deprecated mailcomposer@3.12.0: This project is unmaintained
npm WARN deprecated fsevents@1.2.13: The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2
npm WARN deprecated @types/bson@4.2.0: This is a stub types definition. bson provides its own type definitions, so you do not need this installed.
npm WARN deprecated buildmail@3.10.0: This project is unmaintained
npm WARN deprecated source-map-resolve@0.5.3: See https://github.com/lydell/source-map-resolve#deprecated
npm WARN deprecated resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated
npm WARN deprecated source-map-url@0.4.1: See https://github.com/lydell/source-map-url#deprecated
npm WARN deprecated urix@0.1.0: Please see https://github.com/lydell/urix#deprecated

> sharp@0.28.1 install /srv/app/node_modules/sharp
> (node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)

sharp: Downloading https://github.com/lovell/sharp-libvips/releases/download/v8.10.6/libvips-8.10.6-linux-x64.tar.br

> sqlite3@5.0.0 install /srv/app/node_modules/sqlite3
> node-pre-gyp install --fallback-to-build

node-pre-gyp WARN Using request for node-pre-gyp https download
[sqlite3] Success: "/srv/app/node_modules/sqlite3/lib/binding/napi-v3-linux-x64/node_sqlite3.node" is installed via remote

> @fortawesome/fontawesome-common-types@0.2.36 postinstall /srv/app/node_modules/@fortawesome/fontawesome-common-types
> node attribution.js

Font Awesome Free 0.2.36 by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)


> @fortawesome/fontawesome-free@5.15.4 postinstall /srv/app/node_modules/@fortawesome/fontawesome-free
> node attribution.js

Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)


> @fortawesome/fontawesome-svg-core@1.2.36 postinstall /srv/app/node_modules/@fortawesome/fontawesome-svg-core
> node attribution.js

Font Awesome Free 1.2.36 by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)


> @fortawesome/free-brands-svg-icons@5.15.4 postinstall /srv/app/node_modules/@fortawesome/free-brands-svg-icons
> node attribution.js

Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)


> @fortawesome/free-regular-svg-icons@5.15.4 postinstall /srv/app/node_modules/@fortawesome/free-regular-svg-icons
> node attribution.js

Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)


> @fortawesome/free-solid-svg-icons@5.15.4 postinstall /srv/app/node_modules/@fortawesome/free-solid-svg-icons
> node attribution.js

Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)


> core-js@2.6.12 postinstall /srv/app/node_modules/core-js
> node -e "try{require('./postinstall')}catch(e){}"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> core-js@3.33.0 postinstall /srv/app/node_modules/fbjs/node_modules/core-js
> node -e "try{require('./postinstall')}catch(e){}"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting core-js:
> https://opencollective.com/core-js
> https://patreon.com/zloirock
> https://boosty.to/zloirock
> bitcoin: bc1qlea7544qtsmj2rayg0lthvza9fau63ux0fstcz

I highly recommend reading this: https://github.com/zloirock/core-js/blob/master/docs/2023-02-14-so-whats-next.md


> strapi@3.6.8 postinstall /srv/app/node_modules/strapi
> node lib/utils/success.js

npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@~2.3.1 (node_modules/chokidar/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.3.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@^1.2.7 (node_modules/watchpack-chokidar2/node_modules/chokidar/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@^1.2.7 (node_modules/webpack-dev-server/node_modules/chokidar/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN bootstrap@4.6.2 requires a peer of jquery@1.9.1 - 3 but none is installed. You must install peer dependencies yourself.

added 1739 packages from 2528 contributors and audited 1743 packages in 72.698s

148 packages are looking for funding
  run `npm fund` for details

found 55 vulnerabilities (2 low, 21 moderate, 25 high, 7 critical)
  run `npm audit fix` to fix them, or `npm audit` for details
Starting your app...
Building your admin UI with development configuration ...

βœ” Webpack
  Compiled successfully in 25.59s

[2023-10-19T23:57:21.061Z] info File created: /srv/app/extensions/users-permissions/config/jwt.js

 Project information

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Time               β”‚ Thu Oct 19 2023 23:57:23 GMT+0000 (Coordinated … β”‚
β”‚ Launched in        β”‚ 9771 ms                                          β”‚
β”‚ Environment        β”‚ development                                      β”‚
β”‚ Process PID        β”‚ 177                                              β”‚
β”‚ Version            β”‚ 3.6.8 (node v14.16.0)                            β”‚
β”‚ Edition            β”‚ Community                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

 Actions available

One more thing...
Create your first administrator πŸ’» by going to the administration panel at:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ http://localhost:1337/admin β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

[2023-10-19T23:57:23.304Z] debug HEAD /admin (10 ms) 200
[2023-10-19T23:57:23.307Z] info ⏳ Opening the admin panel...
[2023-10-19T23:57:58.225Z] debug GET /admin (3 ms) 200
[2023-10-19T23:57:58.273Z] debug GET /admin/runtime~main.ae6e3b6c.js (6 ms) 200
[2023-10-19T23:57:58.273Z] debug GET /admin/main.a1d0a632.chunk.js (3 ms) 200
[2023-10-19T23:57:58.871Z] debug GET /admin/init (4 ms) 200
[2023-10-19T23:57:58.881Z] debug GET /favicon.ico (2 ms) 200
[2023-10-19T23:57:59.120Z] debug GET /admin/842e7845f3f8e943ff712a39617b6b70.svg (2 ms) 200
[2023-10-19T23:57:59.138Z] debug GET /admin/07109cdae9f760e8d97c89788c9dc9df.png (2 ms) 200
[2023-10-19T23:57:59.142Z] debug GET /admin/a6069540692725c247f13984a9598a92.woff2 (2 ms) 200
[2023-10-19T23:57:59.155Z] debug GET /admin/21b3848a32fce5b0f5014948186f6964.woff2 (3 ms) 200
[2023-10-19T23:57:59.157Z] debug GET /admin/75614cfcfedd509b1f7ac1c26c53bb7f.woff2 (4 ms) 200

We can now login

/content/images/2023/10/strapi-01.png

Once the account is created, we can see the admin page

/content/images/2023/10/strapi-02.png

Let’s create a user

/content/images/2023/10/strapi-03.png

I’ll add a user jsmith

/content/images/2023/10/strapi-04.png

Now I have two users

/content/images/2023/10/strapi-05.png

while this is fine locally, it won’t be all that usable outside a local dev environment

/content/images/2023/10/strapi-06.png

YAML

$ cat strapi.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: strapi-database-conf
data:
  MYSQL_USER: strapi
  MYSQL_DATABASE: strapi-k8s
---
apiVersion: v1
kind: Secret
metadata:
  name: strapi-database-secret
type: Opaque
data:
  # please NEVER use these passwords, always use strong passwords
  MYSQL_ROOT_PASSWORD: c3RyYXBpLXN1cGVyLXNlY3VyZS1yb290LXBhc3N3b3Jk # echo -n strapi-super-secure-root-password | base64
  MYSQL_PASSWORD: c3RyYXBpLXBhc3N3b3Jk # echo -n strapi-password | base64
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: strapi-app-conf
data:
  HOST: 0.0.0.0
  PORT: "1337"
  NODE_ENV: production

  # we'll explain the db host later
  DATABASE_HOST: strapi-db
  DATABASE_PORT: "3306"
  DATABASE_USERNAME: strapi
  DATABASE_NAME: strapi-k8s
---
apiVersion: v1
kind: Secret
metadata:
  name: strapi-app-secret
type: Opaque
data:
  # use the proper values in here
  APP_KEYS: cXdlcnR5dWlvcGFzZGZnaGprbHp4Y3Zibm0xMjM0NTYK
  API_TOKEN_SALT: cXdlcnR5dWlvcGFzZGZnaGprbHp4Y3Zibm0xMjM0NTYK
  ADMIN_JWT_SECRET: ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKSVV6STFOaUo5LmV5SnBjM01pT2lKUGJteHBibVVnU2xkVUlFSjFhV3hrWlhJaUxDSnBZWFFpT2pFMk9UZ3dNakl3TXpnc0ltVjRjQ0k2TVRjeU9UVTFPREF6T0N3aVlYVmtJam9pZDNkM0xtVjRZVzF3YkdVdVkyOXRJaXdpYzNWaUlqb2lhbkp2WTJ0bGRFQmxlR0Z0Y0d4bExtTnZiU0lzSWtkcGRtVnVUbUZ0WlNJNklrcHZhRzV1ZVNJc0lsTjFjbTVoYldVaU9pSlNiMk5yWlhRaUxDSkZiV0ZwYkNJNkltcHliMk5yWlhSQVpYaGhiWEJzWlM1amIyMGlMQ0pTYjJ4bElqcGJJazFoYm1GblpYSWlMQ0pRY205cVpXTjBJRUZrYldsdWFYTjBjbUYwYjNJaVhYMC5vSExVTF9Db0hjR0c4SXFRRzg0RUw3cURTVE1DQ1hlRGgtQmszZ0ZnWFF3Cg==
  JWT_SECRET: ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKSVV6STFOaUo5LmV5SnBjM01pT2lKUGJteHBibVVnU2xkVUlFSjFhV3hrWlhJaUxDSnBZWFFpT2pFMk9UZ3dNakl3TXpnc0ltVjRjQ0k2TVRjeU9UVTFPREF6T0N3aVlYVmtJam9pZDNkM0xtVjRZVzF3YkdVdVkyOXRJaXdpYzNWaUlqb2lhbkp2WTJ0bGRFQmxlR0Z0Y0d4bExtTnZiU0lzSWtkcGRtVnVUbUZ0WlNJNklrcHZhRzV1ZVNJc0lsTjFjbTVoYldVaU9pSlNiMk5yWlhRaUxDSkZiV0ZwYkNJNkltcHliMk5yWlhSQVpYaGhiWEJzWlM1amIyMGlMQ0pTYjJ4bElqcGJJazFoYm1GblpYSWlMQ0pRY205cVpXTjBJRUZrYldsdWFYTjBjbUYwYjNJaVhYMC5vSExVTF9Db0hjR0c4SXFRRzg0RUw3cURTVE1DQ1hlRGgtQmszZ0ZnWFF3Cg==
  # please NEVER use these passwords, always use strong passwords
  DATABASE_PASSWORD: c3RyYXBpLXBhc3N3b3Jk # echo -n strapi-password | base64
---
# ~/strapi-k8s/k8s/db.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: strapi-database-pv
  labels:
    type: local
spec:
  capacity:
    storage: 5Gi
  storageClassName: managed-nfs-storage
  nfs:
    path: /volume1/k3snfs77b2/default-gitness-data-pvc-6915f5f4-5f87-4468-bf23-16c61a82d2a5
    server: 192.168.1.129
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: strapi-database-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: strapi-db
spec:
  replicas: 1
  selector:
    matchLabels:
      app: strapi-db
  template:
    metadata:
      labels:
        app: strapi-db
    spec:
      containers:
        - name: mysql
          image: mysql:5.7
          securityContext:
            runAsUser: 1000
            allowPrivilegeEscalation: false
          ports:
            - containerPort: 3306
              name: mysql
          envFrom:
            - configMapRef:
                name: strapi-database-conf # the name of our ConfigMap for our db.
            - secretRef:
                name: strapi-database-secret # the name of our Secret for our db.
          volumeMounts:
            - name: mysql-persistent-storage
              mountPath: /var/lib/mysql
      volumes:
        - name: mysql-persistent-storage
          persistentVolumeClaim:
            claimName: strapi-database-pvc # the name of our PersistentVolumeClaim
---
apiVersion: v1
kind: Service
metadata:
  name: strapi-db # this is the name we use for DATABASE_HOST
spec:
  selector:
    app: strapi-db
  ports:
    - name: mysql
      protocol: TCP
      port: 3306
      targetPort: mysql # same name defined in the Deployment path spec.template.spec.containers[0].ports[0].name
---
# ~/strapi-k8s/k8s/app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: strapi-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: strapi-app
  template:
    metadata:
      labels:
        app: strapi-app
    spec:
      containers:
        - name: strapi
          image: elestio/strapi:latest
          ports:
            - containerPort: 1337
              name: http
          envFrom:
            - configMapRef:
                name: strapi-app-conf # the name of our ConfigMap for our app.
            - secretRef:
                name: strapi-app-secret # the name of our Secret for our app.
---
apiVersion: v1
kind: Service
metadata:
  name: strapi-app
spec:
  type: NodePort
  selector:
    app: strapi-app
  ports:
    - name: http
      protocol: TCP
      port: 1337
      nodePort: 31337 # we are using this port, to match the cluster port forwarding section from mycluster.yaml
      targetPort: http # same name defined in the Deployment path spec.template.spec.containers[0].ports[0].name
      

The problem was every time I tried to connect, the pod crashed.

I tried using an online JWT builder but really, I have no idea what to use for the values in the .env

E.g. from one guide, it had an example .env

HOST=0.0.0.0
PORT=1337
APP_KEYS="toBeModified1,toBeModified2"
API_TOKEN_SALT=tobemodified
ADMIN_JWT_SECRET=tobemodified
JWT_SECRET=tobemodified

DATABASE_HOST=strapiDB
DATABASE_PORT=3306

DATABASE_USERNAME=tobemodified
DATABASE_PASSWORD=tobemodified
DATABASE_NAME=tobemodified

NODE_ENV=production

I saw a Strapi blog post that suggested I might just make them random base64 keys.. e.g.

builder@LuiGi17:~/strapi_k8s$ openssl rand -base64 32
9dIBXIi8oxpURT8264nEN+Al73vP7SjNmCS/UXRmwGY=
builder@LuiGi17:~/strapi_k8s$ openssl rand -base64 32
Q8ZWhdnhSK2hqi/k8Nu7NQrqp1/0MBDPdp8ZJkdWGhg=
builder@LuiGi17:~/strapi_k8s$ openssl rand -base64 32
C0VUa6T+oyt0BokT0wzBjpe253ow9vp/IphsO2cqVNc=
builder@LuiGi17:~/strapi_k8s$ openssl rand -base64 32
iiAi5uxYowvVcz/HtWQczRW0fFD5jptcm/T/t4cqu6g=

I set the values and tried re-creating

$ kubectl create ns strapi
namespace/strapi created

$ kubectl apply -f strapi.yaml -n strapi
configmap/strapi-database-conf created
secret/strapi-database-secret created
configmap/strapi-app-conf created
secret/strapi-app-secret created
persistentvolume/strapi-database-pv unchanged
persistentvolumeclaim/strapi-database-pvc created
deployment.apps/strapi-db created
service/strapi-db created
deployment.apps/strapi-app created
service/strapi-app created

Nicht zer gut…

$ kubectl get pods -n strapi
NAME                          READY   STATUS             RESTARTS      AGE
strapi-db-5fc8578b55-dj8l4    1/1     Running            0             74s
strapi-app-5bcbd45cb4-v48v5   0/1     CrashLoopBackOff   3 (30s ago)   74s

$ kubectl logs strapi-app-5bcbd45cb4-v48v5 -n strapi
/bin/sh: 1: ./entrypoint.sh: not found

strapi-development:latest was better:

$ kubectl get pods -n strapi
NAME                         READY   STATUS    RESTARTS     AGE
strapi-db-5fc8578b55-dj8l4   1/1     Running   0            5m48s
strapi-app-d8bbbdcf6-g7dv4   1/1     Running   1 (8s ago)   103s

It vomits each time on first usage

$ kubectl logs strapi-app-d8bbbdcf6-g7dv4 -n strapi
... snip ...

Done in 25.67s.
yarn run v1.22.19
$ strapi develop
node:assert:172
  throw err;
  ^

AssertionError [ERR_ASSERTION]: unknown message code: 4a
    at Parser.handlePacket (/opt/app/node_modules/pg-protocol/dist/parser.js:140:34)
    at Parser.parse (/opt/app/node_modules/pg-protocol/dist/parser.js:39:38)
    at Socket.<anonymous> (/opt/app/node_modules/pg-protocol/dist/index.js:11:42)
    at Socket.emit (node:events:517:28)
    at Socket.emit (node:domain:489:12)
    at addChunk (node:internal/streams/readable:335:12)
    at readableAddChunk (node:internal/streams/readable:308:9)
    at Readable.push (node:internal/streams/readable:245:10)
    at TCP.onStreamRead (node:internal/stream_base_commons:190:23) {
  generatedMessage: false,
  code: 'ERR_ASSERTION',
  actual: undefined,
  expected: undefined,
  operator: 'fail'
}

Node.js v18.18.2
Done in 5.07s.

I found a Vultr article on doing it a bit more manually. It is a bit dated (ie. Node 18 is minimum now and the guide uses 16).

I’ll use nvm to set 18.18.1 and then create an instance with npx

builder@LuiGi17:~/strapi_k8s2$ nvm use 18.18.1
Now using node v18.18.1 (npm v9.8.1)

builder@LuiGi17:~/strapi_k8s2$ npx create-strapi-app@latest strapi-project
? Choose your installation type Quickstart (recommended)
Creating a quickstart project.
Creating a new Strapi application at /home/builder/strapi_k8s2/strapi-project.
Creating files.
β § Installing dependencies: npm WARN config optional Use `--omit=optional` to exclude optional dependencies, or npm WARN config `--include=optional` to include them. npm WARN config  npm WARN config     Default value does install optional deps unless otherwise omitted.

In the Vultr guide they move right to hardcoding PostgreSQL databases.

Since this is mostly for learning, I’ll leave in the sqlite DB presently set:

builder@LuiGi17:~/strapi_k8s2/strapi-project$ cat config/database.js
const path = require('path');

module.exports = ({ env }) => {
  const client = env('DATABASE_CLIENT', 'sqlite');

So we can create proper links in pages, add a STRAPI_URL env var to the server.js

builder@LuiGi17:~/strapi_k8s2/strapi-project$ vi config/server.js
builder@LuiGi17:~/strapi_k8s2/strapi-project$ cat config/server.js
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),
  },
});

I’ll then create a Node v18 Dockefile we can use

builder@LuiGi17:~/strapi_k8s2/strapi-project$ vi Dockerfile
builder@LuiGi17:~/strapi_k8s2/strapi-project$ echo 'node_modules/' > .dockerignore
builder@LuiGi17:~/strapi_k8s2/strapi-project$ cat Dockerfile
FROM node:18-alpine

RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev nasm bash vips-dev
ARG NODE_ENV=development
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"]

I’ll likely build and push to Harbor later. But for now, let’s build and push to dockerhub

builder@LuiGi17:~/strapi_k8s2/strapi-project$ docker build -t idjohnson/strapidev:latest .
[+] Building 141.6s (15/15) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                  0.2s
 => => transferring dockerfile: 466B                                                                                                                                  0.0s
 => [internal] load .dockerignore                                                                                                                                     0.2s
 => => transferring context: 54B                                                                                                                                      0.0s
 => [internal] load metadata for docker.io/library/node:18-alpine                                                                                                     3.0s
 => [auth] library/node:pull token for registry-1.docker.io                                                                                                           0.0s
 => [internal] load build context                                                                                                                                     0.1s
 => => transferring context: 1.26MB                                                                                                                                   0.1s
 => [1/9] FROM docker.io/library/node:18-alpine@sha256:435dcad253bb5b7f347ebc69c8cc52de7c912eb7241098b920f2fc2d7843183d                                              11.8s
 => => resolve docker.io/library/node:18-alpine@sha256:435dcad253bb5b7f347ebc69c8cc52de7c912eb7241098b920f2fc2d7843183d                                               0.0s
 => => sha256:435dcad253bb5b7f347ebc69c8cc52de7c912eb7241098b920f2fc2d7843183d 1.43kB / 1.43kB                                                                        0.0s
 => => sha256:51490771aba658439d29b1b03b60fc31e67bf0da3e01cb5903716310df4be1c1 1.16kB / 1.16kB                                                                        0.0s
 => => sha256:d1517ab6615b781f3b81f339100063d1b2b41f1a32a9efb8563ecd1375311c22 6.78kB / 6.78kB                                                                        0.0s
 => => sha256:96526aa774ef0126ad0fe9e9a95764c5fc37f409ab9e97021e7b4775d82bf6fa 3.40MB / 3.40MB                                                                        0.9s
 => => sha256:3130715204cf4a9be94608d180505f50862416589a8f03eba7b664f15b9c0283 47.88MB / 47.88MB                                                                      8.5s
 => => sha256:b06de8ab1c4feccaf7b687bb7ebd5180c2bd1f59d91749619d52af77fd38ea13 2.34MB / 2.34MB                                                                        4.0s
 => => extracting sha256:96526aa774ef0126ad0fe9e9a95764c5fc37f409ab9e97021e7b4775d82bf6fa                                                                             0.1s
 => => sha256:90ef3ffc51561ffa7dfafd2dc93f44601f8d4d4273ad8d54dbf34326c746a142 448B / 448B                                                                            1.8s
 => => extracting sha256:3130715204cf4a9be94608d180505f50862416589a8f03eba7b664f15b9c0283                                                                             2.5s
 => => extracting sha256:b06de8ab1c4feccaf7b687bb7ebd5180c2bd1f59d91749619d52af77fd38ea13                                                                             0.1s
 => => extracting sha256:90ef3ffc51561ffa7dfafd2dc93f44601f8d4d4273ad8d54dbf34326c746a142                                                                             0.0s
 => [2/9] RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev nasm bash vips-dev                                               51.7s
 => [3/9] WORKDIR /opt/                                                                                                                                               0.1s
 => [4/9] COPY package.json package-lock.json ./                                                                                                                      0.1s
 => [5/9] RUN npm install                                                                                                                                            42.5s
 => [6/9] WORKDIR /opt/app                                                                                                                                            0.1s
 => [7/9] COPY . .                                                                                                                                                    0.2s
 => [8/9] RUN chown -R node:node /opt/app                                                                                                                             0.9s
 => [9/9] RUN ["npm", "run", "build"]                                                                                                                                21.4s
 => exporting to image                                                                                                                                                9.4s
 => => exporting layers                                                                                                                                               9.4s
 => => writing image sha256:4f481904b0acf9b57835b7c7f8d62f569798f2cd48bc598f18dbcec240458092                                                                          0.0s
 => => naming to docker.io/idjohnson/strapidev:latest

I made the foolish mistake of using my cell hotspot to try and push the first time. Burned through way too much data. Best to do this at home or on wifi

builder@LuiGi17:~/strapi_k8s2/strapi-project$ docker push idjohnson/strapidev:latest
The push refers to repository [docker.io/idjohnson/strapidev]
94f90a4d252d: Pushed
9f575e0e261e: Pushed
9092b0bd387a: Pushed
211eb9042d74: Pushed
fda1ff26d495: Pushed
708504658f09: Pushed
5f70bf18a086: Mounted from elestio/strapi-development
d9c53ce14338: Pushed
b76efd4eddd5: Mounted from library/node
fa5569ec60d1: Mounted from library/node
b105890ed2f2: Mounted from library/node
cc2447e1835a: Mounted from library/node
latest: digest: sha256:30f0ad6f00a1a4567a09cc3f408020f4eeecbb58562a65b89b97ba8f52f51563 size: 2838

We can now create a deployment YAML to use the image.

$ cat strapi-deployment.yaml
 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: strapi-deployment
 spec:
   replicas: 1
   selector:
     matchLabels:
       name: strapi-app
   template:
     metadata:
       labels:
         name: strapi-app
     spec:
       containers:
         - name: strapi
           image: idjohnson/strapidev:latest
           imagePullPolicy: Always
           command: ["npm", "run", "build", "&&", "npm", "run", "develop"]
           ports:
             - containerPort: 1337
           env:
             - name: STRAPI_URL
               value: "strapi.freshbrewed.science"

I’ll apply it and see if the container comes up

$ kubectl apply -f strapi-deployment.yaml
deployment.apps/strapi-deployment created

$ kubectl get pods | grep strapi
strapi-deployment-759bbc4bd8-jmr7d                       1/1     Running   1 (10s ago)       65s

And as before, it crashes

builder@LuiGi17:~/strapi_k8s2/strapi-project$ kubectl port-forward strapi-deployment-759bbc4bd8-jmr7d 1337:1337
Forwarding from 127.0.0.1:1337 -> 1337
Forwarding from [::1]:1337 -> 1337
Handling connection for 1337
Handling connection for 1337
E1025 19:01:54.813194   68374 portforward.go:409] an error occurred forwarding 1337 -> 1337: error forwarding port 1337 to pod 9d5434344f9d9a8895c175afc2c39f55ee2851117204cf949a193fe534c5ef3a, uid : failed to execute portforward in network namespace "/var/run/netns/cni-25b1c4ac-01d7-4108-38cf-6600ca5635f0": failed to connect to localhost:1337 inside namespace "9d5434344f9d9a8895c175afc2c39f55ee2851117204cf949a193fe534c5ef3a", IPv4: dial tcp4 127.0.0.1:1337: connect: connection refused IPv6 dial tcp6: address localhost: no suitable address found
E1025 19:01:54.813197   68374 portforward.go:409] an error occurred forwarding 1337 -> 1337: error forwarding port 1337 to pod 9d5434344f9d9a8895c175afc2c39f55ee2851117204cf949a193fe534c5ef3a, uid : failed to execute portforward in network namespace "/var/run/netns/cni-25b1c4ac-01d7-4108-38cf-6600ca5635f0": failed to connect to localhost:1337 inside namespace "9d5434344f9d9a8895c175afc2c39f55ee2851117204cf949a193fe534c5ef3a", IPv4: dial tcp4 127.0.0.1:1337: connect: connection refused IPv6 dial tcp6: address localhost: no suitable address found
error: lost connection to pod
builder@LuiGi17:~/strapi_k8s2/strapi-project$ kubectl logs strapi-deployment-759bbc4bd8-jmr7d

> strapi-project@0.1.0 build
> strapi build && npm run develop

Building your admin UI with development configuration...
β„Ή Compiling Webpack
βœ” Webpack: Compiled successfully in 22.75s
Admin UI built successfully
builder@LuiGi17:~/strapi_k8s2/strapi-project$ kubectl get pods | grep strapi
strapi-deployment-759bbc4bd8-jmr7d                       0/1     Completed   2 (52s ago)       2m18s
builder@LuiGi17:~/strapi_k8s2/strapi-project$ kubectl logs strapi-deployment-759bbc4bd8-jmr7d

> strapi-project@0.1.0 build
> strapi build && npm run develop

Building your admin UI with development configuration...
β„Ή Compiling Webpack
βœ” Webpack: Compiled successfully in 22.75s
Admin UI built successfully
builder@LuiGi17:~/strapi_k8s2/strapi-project$ kubectl get pods | grep strapi
strapi-deployment-759bbc4bd8-jmr7d                       0/1     CrashLoopBackOff   2 (20s ago)       2m30s
builder@LuiGi17:~/strapi_k8s2/strapi-project$ kubectl logs strapi-deployment-759bbc4bd8-jmr7d

> strapi-project@0.1.0 build
> strapi build && npm run develop

Building your admin UI with development configuration...
β„Ή Compiling Webpack
βœ” Webpack: Compiled successfully in 22.75s
Admin UI built successfully

It dawned on me that the β€˜build’ was unnecessary

           command: ["npm", "run", "build", "&&", "npm", "run", "develop"]

I changed the command to just


           command: ["npm", "run", "develop"]

This time it stayed up

builder@LuiGi17:~/strapi_k8s2/strapi-project$ kubectl get pods | grep strapi
strapi-deployment-566d569f9d-5xrrc                       1/1     Running   0                 16s
builder@LuiGi17:~/strapi_k8s2/strapi-project$ kubectl port-forward strapi-deployment-566d569f9d-5xrrc 1337:1337
Forwarding from 127.0.0.1:1337 -> 1337
Forwarding from [::1]:1337 -> 1337
Handling connection for 1337
Handling connection for 1337
Handling connection for 1337
Handling connection for 1337

/content/images/2023/10/strapi-07.png

To actually resolve the images and use the Ingress, I’ll need to create a quick A record

$ cat r53-strapi.json
{
    "Comment": "CREATE strapi fb.s A record ",
    "Changes": [
      {
        "Action": "CREATE",
        "ResourceRecordSet": {
          "Name": "strapi.freshbrewed.science",
          "Type": "A",
          "TTL": 300,
          "ResourceRecords": [
            {
              "Value": "75.73.224.240"
            }
          ]
        }
      }
    ]
  }

$ aws route53 change-resource-record-sets --hosted-zone-id Z39E8QFU0F9PZP --change-batch file://r53-strapi.json
{
    "ChangeInfo": {
        "Id": "/change/C101378827FQ27XFVVH9Z",
        "Status": "PENDING",
        "SubmittedAt": "2023-10-26T00:10:29.318Z",
        "Comment": "CREATE strapi fb.s A record "
    }
}

Now I can create the Service and Ingress

$ cat svc_and_ingress.yaml
apiVersion: v1
kind: Service
metadata:
   name: strapi-service
spec:
   ports:
     - name: http
       port: 80
       protocol: TCP
       targetPort: 1337
   selector:
     name: strapi-app
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.org/websocket-services: strapi-service
  labels:
    app.kubernetes.io/instance: strapiingress
  name: strapiingress
spec:
  rules:
  - host: strapi.freshbrewed.science
    http:
      paths:
      - backend:
          service:
            name: strapi-service
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - strapi.freshbrewed.science
    secretName: strapi-tls

$ kubectl apply -f svc_and_ingress.yaml
service/strapi-service created
ingress.networking.k8s.io/strapiingress created

/content/images/2023/10/strapi-08.png

That does not see quite right

/content/images/2023/10/strapi-09.png

I’ll try the FQDN this time

builder@LuiGi17:~/strapi_k8s2/strapi-project$ cat strapi-deployment.yaml
 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: strapi-deployment
 spec:
   replicas: 1
   selector:
     matchLabels:
       name: strapi-app
   template:
     metadata:
       labels:
         name: strapi-app
     spec:
       containers:
         - name: strapi
           image: idjohnson/strapidev:latest
           imagePullPolicy: Always
           command: ["npm", "run", "develop"]
           ports:
             - containerPort: 1337
           env:
             - name: STRAPI_URL
               value: "https://strapi.freshbrewed.science"
builder@LuiGi17:~/strapi_k8s2/strapi-project$ kubectl apply -f strapi-deployment.yaml
deployment.apps/strapi-deployment configured

much better

/content/images/2023/10/strapi-10.png

We can test it and see that a pod rotation also kills the database (which I expected as I didn’t setup PSQL yet)

Setting up a database

postgres@isaac-MacBookAir:~$ createuser --pwprompt strapi
Enter password for new role:
Enter it again:

Create DB

postgres@isaac-MacBookAir:~$ createdb strapidb
postgres@isaac-MacBookAir:~$ psql
psql (12.16 (Ubuntu 12.16-0ubuntu0.20.04.1))
Type "help" for help.

postgres=# GRANT ALL ON ALL TABLES IN SCHEMA public TO strapi;
GRANT
postgres=# grant all privileges on database strapi to strapi;
ERROR:  database "strapi" does not exist
postgres=# grant all privileges on database strapidb to strapi;
GRANT

We then need to update the database.js file to use PostgreSQL

builder@LuiGi17:~/strapi_k8s2/strapi-project$ cat config/database.js
module.exports = ({ env }) => ({
   connection: {
       client: 'postgres',
       connection: {
           host: env('DATABASE_HOST', '192.168.1.78'),
           port: env.int('DATABASE_PORT', 5432),
           database: env('DATABASE_NAME', 'strapidb'),
           user: env('DATABASE_USERNAME', 'strapi'),
           password: env('DATABASE_PASSWORD', 'password'),
           schema: env('DATABASE_SCHEMA', 'public'), // Not required
           ssl: {
               rejectUnauthorized: env.bool('DATABASE_SSL_SELF', false),
           },
       },
       debug: false,
    },
   });

Now I can build

$ docker build -t harbor.freshbrewed.science/freshbrewedprivate/strapidev:v2 .

and push the container:

builder@LuiGi17:~/strapi_k8s2/strapi-project$ docker login harbor.freshbrewed.science
Username: isaac
Password:
Login Succeeded

builder@LuiGi17:~/strapi_k8s2/strapi-project$ docker push harbor.freshbrewed.science/freshbrewedprivate/strapidev:v2
The push refers to repository [harbor.freshbrewed.science/freshbrewedprivate/strapidev]
0f90184e4d19: Pushed
7305264fd090: Pushed
03a5dc1df2c8: Pushed
211eb9042d74: Pushed
fda1ff26d495: Pushed
708504658f09: Pushed
5f70bf18a086: Pushed
d9c53ce14338: Pushed
b76efd4eddd5: Pushed
fa5569ec60d1: Pushed
b105890ed2f2: Pushed
cc2447e1835a: Pushed
v2: digest: sha256:e21d46b7b029d1349f0d4a3a5c53cd0f199fdf45d9ad65b1c8aecd4b3f97e4d1 size: 2838

Now I can use it locally. I will need to set an ImagePullSecret for a private registry

$ cat strapi-deployment.yaml
 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: strapi-deployment
 spec:
   replicas: 1
   selector:
     matchLabels:
       name: strapi-app
   template:
     metadata:
       labels:
         name: strapi-app
     spec:
       imagePullSecrets:
         - name: myharborreg
       containers:
         - name: strapi
           image: harbor.freshbrewed.science/freshbrewedprivate/strapidev:v2
           command: ["npm", "run", "develop"]
           ports:
             - containerPort: 1337
           env:
             - name: STRAPI_URL
               value: "https://strapi.freshbrewed.science"

$ kubectl apply -f strapi-deployment.yaml
deployment.apps/strapi-deployment configured

The pod endlessly crashed

/content/images/2023/10/strapi-12.png

It dawned on me that it might need psql installed as a node module

builder@LuiGi17:~/strapi_k8s2/strapi-project$ npm install pg --save

added 23 packages, and audited 1447 packages in 3s

167 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

I’ll build and push

$ docker build -t harbor.freshbrewed.science/freshbrewedprivate/strapidev:v3 .
$ docker push harbor.freshbrewed.science/freshbrewedprivate/strapidev:v3

This time it stayed up

/content/images/2023/10/strapi-13.png

I’ll create an account and an entry then rotate the pod and see

/content/images/2023/10/strapi-15.png

Summary

We got started with Strapi CMS locally then with Docker. Then we worked through Kubernetes deployments before building a local container (which worked). Lastly, we created a container with Database settings and used it with a private CR.

Strapi has the ability to expose the content with GraphQL which we will explore later:

/content/images/2023/10/strapi-16.png

CMS Strapi Kubernetes

Have something to add? Feedback? Try our new forums

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes