![SREDevOps.org](https://private-user-images.githubusercontent.com/34670018/277867298-6878e00f-635c-4553-8df7-3b20406fdb4f.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQwMDUxOTIsIm5iZiI6MTcyNDAwNDg5MiwicGF0aCI6Ii8zNDY3MDAxOC8yNzc4NjcyOTgtNjg3OGUwMGYtNjM1Yy00NTUzLThkZjctM2IyMDQwNmZkYjRmLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA4MTglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwODE4VDE4MTQ1MlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTRhMGQ3NWVkNGYzYThiMGNlZDFkYTgxNTQ3YTdlNmFkOTkwMDdmMzI1ZjU1NzE0YmM3NmU0OGZkZGE0OWRjNzAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.yybRiGwbV7BAiM41AFXYmvswBzRIjzL24HN_RkOkSw0)
Community for SRE, DevOps, Cloud Native, GNU/Linux, and more. ๐
This repository implements Ghost CMS v5.xx.x from @TryGhost (upstream) on Kubernetes, with our custom image, which has significant improvements to be used on Kubernetes. See this whole README for more information.
We've made some significant updates to improve the security and efficiency of our Ghost implementation on Kubernetes:
- Multi-arch support: The images are now multi-arch, with support for amd64 and arm64.
- Distroless Image: We use @GoogleContainerTools's Distroless NodeJS as the execution environment for the final image. Distroless images are minimal images that contain only the necessary components to run the application, making them more secure and efficient than traditional images.
- MySQL StatefulSet: We've changed the MySQL implementation to a StatefulSet. This provides stable network identifiers and persistent storage, which is important for databases like MySQL that need to maintain state.
- Init Container: We've added an init container to the Ghost deployment. This container is responsible for setting up the necessary configuration files and directories before the main Ghost container starts, ensuring the right directories are created, correct ownership for user node inside distroless container UID/GID to 65532, and the correct permissions are set. Check deploy/06-ghost-deployment.yaml for details on these changes.
- Entrypoint Script: We've introduced a new entrypoint script that runs as the non-privileged user inside the distroless container. This script is responsible for updating the default themes then starts the Ghost application. This script is executed by the nonroot user without privileges within the Distroless container, which updates default themes and starts the Ghost application, operation performed into the distroless container in runtime. entrypoint.js
#
# This Dockerfile is used to build a container image for running Ghost, a popular open-source blogging platform, on Kubernetes.
# The image is built with the official Node 20 on Debian Bookworm (LTS Iron) image and uses the Distroless base image for security and minimalism.
#
# Stage 1: Build Environment
# In this stage, the build environment is set up and the necessary dependencies are installed.
# The Ghost version is defined as a build argument and set as an environment variable.
# The installation directory, content directory, and original content directory for Ghost are also set as environment variables.
# The Ghost CLI is installed globally and configured with some workarounds to build the arm64 version in GitHub without timeout failures.
# Ghost is then installed with the specified version, using MySQL as the database, and configured without prompts, stack traces, and setup.
# The original content directory is moved to a backup location, a new content directory is created, and the correct ownership and permissions are set.
#
# Stage 2: Final Image
# In this stage, the final image is created using the Distroless base image.
# The Ghost installation directory is copied from the build environment to the final image.
# The working directory is set to the Ghost installation directory and a volume is created for the content directory.
# The entrypoint script is copied to the current Ghost version.
# Port 2368 is exposed for Ghost.
# The command is set to start Ghost with the entrypoint script.
#
# For more information, refer to the GitHub repository: https://github.com/sredevopsorg/ghost-on-kubernetes
# Stage 1: Build Environment
FROM node:iron-bookworm@sha256:786005cf39792f7046bcd66491056c26d2dbcc669c072d1a1e4ef4fcdddd26eb AS build-env
...
# Stage 2: Final Image
FROM gcr.io/distroless/nodejs20-debian12
...
# This Dockerfile is used to build a container image for running Ghost, a popular open-source blogging platform, on Kubernetes.
# The image is built with official Node 20 on Debian Bookworm (LTS Iron) image and uses the Distroless base image for security and minimalism.
# Stage 1: Build Environment
FROM node:iron-bookworm@sha256:786005cf39792f7046bcd66491056c26d2dbcc669c072d1a1e4ef4fcdddd26eb AS build-env
USER root
# Create a new user and group named "nonroot" with the UID 65532 and GID 65532, not a member of the root, sudo, and sys groups, and set the home directory to /home/nonroot.
# This user is used to run the Ghost application in the container for security reasons.
RUN groupadd -g 65532 nonroot && \
useradd -u 65532 -g 65532 -d /home/nonroot nonroot && \
usermod -aG nonroot nonroot && \
mkdir -pv /home/nonroot && \
chown -Rfv 65532:65532 /home/nonroot
USER nonroot
SHELL ["/bin/bash", "-c"]
ENV NODE_ENV=production NPM_CONFIG_LOGLEVEL=info
# Define the GHOST_VERSION build argument and set it as an environment variable
ARG GHOST_VERSION
ENV GHOST_VERSION=$GHOST_VERSION
# Set the installation directory, content directory, and original content directory for Ghost
ENV GHOST_INSTALL=/home/nonroot/app/ghost
ENV GHOST_CONTENT=/home/nonroot/app/ghost/content
ENV GHOST_CONTENT_ORIGINAL=/home/nonroot/app/ghost/content.orig
RUN mkdir -pv "$GHOST_INSTALL"
# Install the latest version of Ghost CLI globally and config some workarounds to build arm64 version in Github without timeout failures
RUN yarn config set network-timeout 60000 && \
yarn config set inline-builds true && \
npm config set fetch-timeout 60000 && \
npm config set progress && \
npm config set omit dev
# Create the Ghost installation directory and set the owner to the "node" user
# RUN npm i -g ghost-cli@latest || yarn global add ghost-cli@latest
# Install Ghost with the specified version, using MySQL as the database, and configure it without prompts, stack traces, setup, and in the specified installation directory
# RUN ghost install $GHOST_VERSION --dir $GHOST_INSTALL --db mysql --dbhost mysql --no-prompt --no-stack --no-setup --color --process local ||
RUN npx ghost-cli install $GHOST_VERSION --dir $GHOST_INSTALL --db mysql --dbhost mysql --no-prompt --no-stack --no-setup --color --process local
# Move the original content directory to a backup location, create a new content directory, set the correct ownership and permissions, and switch back to the "node" user
RUN mv -v $GHOST_CONTENT $GHOST_CONTENT_ORIGINAL && \
mkdir -v $GHOST_CONTENT && \
# chown -Rfv 65532 $GHOST_CONTENT_ORIGINAL && \
# chown -Rfv 65532 $GHOST_CONTENT && \
# chown -fv 65532 $GHOST_INSTALL && \
chmod -v 0775 $GHOST_CONTENT
# Switch back to the "node" user
# USER node
# Stage 2: Final Image
FROM gcr.io/distroless/nodejs20-debian12
# Set the installation directory and content directory for Ghost
ENV GHOST_INSTALL_SRC=/home/nonroot/app/ghost
ENV GHOST_INSTALL=/home/nonroot/app/ghost
ENV GHOST_CONTENT=/home/nonroot/app/ghost/content
ENV GHOST_CONTENT_ORIGINAL=/home/nonroot/app/ghost/content.orig
USER nonroot
# Copy the Ghost installation directory from the build environment to the final image
COPY --from=build-env $GHOST_INSTALL_SRC $GHOST_INSTALL
# Set the working directory to the Ghost installation directory and create a volume for the content directory
# The volume is used to persist the data across container restarts, upgrades, and migrations.
# It's going to be handled with an init container that will copy the content from your original content directory to the new content directory (If there is any)
# The CMD script will handle default themes included (Casper and Source) and init Ghost.
WORKDIR $GHOST_INSTALL
VOLUME $GHOST_CONTENT
# Copy the entrypoint script to the current Ghost version.
COPY --chown=65532 entrypoint.js current/entrypoint.js
# Expose port 2368 for Ghost
EXPOSE 2368
# Set the command to start Ghost with the entrypoint (See https://github.com/sredevopsorg/ghost-on-kubernetes/blob/main/entrypoint.js)
CMD ["current/entrypoint.js"]
- ARM64 Support!
- We use the official Node 20 Iron Bookworm image as our build environment. Dockerfile
- We introduce a multi-stage build, which reduces the final image size, and improves security by removing unnecessary components from the final image.
- Distroless Node 20 Debian 12 as our runtime environment for the final stage of the image.
- Removed gosu, now everything runs as the default Node user (UID 1000:GID 1000) inside the Distroless container. This change itself reduces 2 critical vulnerabilities and 33 high vulnerabilities reported by Docker Scout in the original Ghost image (References: Ghost Official Image and Ghost on Kubernetes Image on Docker Hub at the time of writing).
- New Entrypoint flow, using a Node.js script executed by the Node user without privileges within the Distroless container, which updates default themes and starts the Ghost application, operation which is performed into the distroless container itself.
- We use the latest version of Ghost 5 (when the image is built).
- Images are now multi-arch, with support for amd64 and arm64 (link to discussion)
# Clone the repository
git clone https://github.com/sredevopsorg/ghost-on-kubernetes.git --depth 1 --branch main --single-branch --no-tags
# Change directory
cd ghost-on-kubernetes
# Create a new branch for your changes (optional but recommended).
git checkout -b my-branch --no-track --detach
- There are some example configuration files in the examples directory. We use the stored configuration as a
kind: Secret
in theghost-on-kubernetes
namespace for Ghost and MySQL configuration. There are two example configuration files:config.development.sample.yaml
: This configuration file is for the Ghost development environment. It uses SQLite as the database. It can be useful if you want to test the Ghost configuration before implementing it in a production environment.config.production.sample.yaml
: This configuration file is for the Ghost production environment. It uses MySQL 8, and is the recommended configuration for production environments. It requires a valid top-level domain (TLD) and configuration for Ingress to access Ghost from the Internet.
- If you need more information on the configuration, check the official Ghost documentation.
deploy/00-namespace.yaml
# Change the namespace according to your needsdeploy/01-secrets.yaml
# deploy/01-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: mysql-ghost-on-kubernetes
namespace: ghost-on-kubernetes
type: Opaque
stringData:
MYSQL_DATABASE: mysql-db-name # Same as in deploy/04-config.production.yaml
MYSQL_USER: mysql-db-user # Same as in deploy/04-config.production.yaml
MYSQL_PASSWORD: mysql-db-password # Same as in deploy/04-config.production.yaml
MYSQL_ROOT_PASSWORD: mysql-db-root-password # Same as in deploy/04-config.production.yaml
deploy/02-pvc.yaml
# Change the storageClassName according to your needsdeploy/03-services.yaml
# Change the hosts according to your needsdeploy/04-config.production.yaml
# Change the values according to the secrets and servicesdeploy/05-mysql.yaml
# Change the values according to the secrets and servicesdeploy/06-ghost-deployment.yaml
# Change the values according to the secrets and servicesdeploy/07-ingress.yaml
# Optional
# Before applying the manifests, make sure you are in the root directory of the repository
# ๐จ Be sure to not change the filenames, also be sure to modify the files according to your needs before applying them.
# Why? Just because we need to deploy them in order. If you change the filenames, you will need to apply them one by one in the correct order.
kubectl apply -f ./deploy
# Get the ingress IP, if you have configured the Ingress
kubectl get ingress -n ghost-on-kubernetes -o wide
# Alternatively, create a port-forwarding rule to access the Ghost CMS
kubectl port-forward -n ghost-on-kubernetes service/service-ghost-on-kubernetes 2368:2368
- http://localhost:2368 (if you used the port-forwarding method)
- http://your-ghost-domain.com (if you used the Ingress method)
- http://localhost:2368/ghost (if you used the port-forwarding method)
- http://your-ghost-domain.com/ghost (if you used the Ingress method)
We welcome contributions from the community! Please check the CONTRIBUTING.md file for more information on how to contribute to this project.
- This project is licensed under the GNU General Public License v3.0. Please check the LICENSE file for more information.
- The Ghost CMS is licensed under the MIT License.
- The node:20 image and the Distroless image are licensed by their respective owners.