Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Dockerfile #212

Merged
merged 18 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-pr-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Build tests

on:
pull_request:
types: [opened, synchronize, reopened, edited]
types: [opened, reopened, edited]
branches:
- main
paths:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/multi-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ jobs:
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
--annotation='index:org.opencontainers.image.description="Ghost on Kubernetes by SREDevOps.org' \
--annotation='index:org.opencontainers.image.description="Ghost on Kubernetes by SREDevOps.org https://sredevops.org "' \
$(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *)

-
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,4 @@ Temporary Items

***/*.local*
docker-compose.yml
node_modules
87 changes: 42 additions & 45 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,74 +3,71 @@

# 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

ENV NODE_ENV=production DEBIAN_FRONTEND=noninteractive

# Update sources and install libvips to build some dependencies later
# Define the GHOST_VERSION build argument and set it as an environment variable
ARG GHOST_VERSION
ENV GHOST_VERSION=$GHOST_VERSION

USER root
RUN apt update && apt install --no-install-recommends --no-install-suggests -y libvips-dev
# 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 fetch-timeout 60000 && \
npm config set progress && \
npm config set omit dev

RUN yarn global add ghost-cli@latest

# Define the GHOST_VERSION build argument and set it as an environment variable
ARG GHOST_VERSION
ENV GHOST_VERSION $GHOST_VERSION
# Create the Ghost installation directory and set the owner to the "node" user

# Set the installation directory, content directory, and original content directory for Ghost
ENV GHOST_INSTALL=/var/lib/ghost
ENV GHOST_CONTENT=/var/lib/ghost/content
ENV GHOST_CONTENT_ORIGINAL=/var/lib/ghost/content.orig

# Create the Ghost installation directory and set the owner to the "node" user
RUN mkdir -pv "$GHOST_INSTALL" && \
chown node:node "$GHOST_INSTALL"

# Switch to the "node" user
USER node
# Workarounds to build arm64 version in Github without timeout failures
RUN yarn config set network-timeout 180000 && \
yarn config set inline-builds true && \
npm config set fetch-timeout 180000 && \
npm config set progress && \
npm config set omit dev
# 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 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

# Switch back to the root user
USER root

# 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 -pv $GHOST_CONTENT && \
chown -Rfv node:node $GHOST_CONTENT_ORIGINAL && \
chown -Rfv node:node $GHOST_CONTENT && \
chown -fv node:node $GHOST_INSTALL && \
chmod 1775 $GHOST_CONTENT
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
# USER node

# Stage 2: Final Image
FROM gcr.io/distroless/nodejs20-debian12@sha256:08d0b6846a21812d07a537eff956acc1bc38a7440a838ce6730515f8d3cd5d9e AS runtime
FROM gcr.io/distroless/nodejs20-debian12

# Set the installation directory and content directory for Ghost
ENV GHOST_INSTALL=/var/lib/ghost
ENV GHOST_CONTENT=/var/lib/ghost/content
ENV GHOST_CONTENT_ORIGINAL=/var/lib/ghost/content.orig

USER node
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 $GHOST_INSTALL
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.
Expand All @@ -81,11 +78,11 @@ WORKDIR $GHOST_INSTALL
VOLUME $GHOST_CONTENT

# Copy the entrypoint script to the current Ghost version.
COPY --chown=1000:1000 entrypoint.js current/entrypoint.js
COPY --chown=65532 entrypoint.js current/entrypoint.js


# Expose port 2368 for Ghost
EXPOSE 2368

# Set the command to start Ghost
# 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"]
129 changes: 125 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Ghost on Kubernetes by SREDevOps.Org

<center><a href="https://sredevops.org" target="_blank" rel="noopener noreferrer"><img src="https://github.com/sredevopsorg/.github/assets/34670018/6878e00f-635c-4553-8df7-3b20406fdb4f" alt="SREDevOps.org" width="60%" align="center" /></a></center>
<center><a href="https://sredevops.org" target="_blank" rel="noopener"><img src="https://github.com/sredevopsorg/.github/assets/34670018/6878e00f-635c-4553-8df7-3b20406fdb4f" alt="SREDevOps.org" width="60%" align="center" /></a></center>

**Community for SRE, DevOps, Cloud Native, GNU/Linux, and more. 🌎**

[![Build Multiarch](https://github.com/sredevopsorg/ghost-on-kubernetes/actions/workflows/multi-build.yaml/badge.svg?branch=main)](https://github.com/sredevopsorg/ghost-on-kubernetes/actions/workflows/multi-build.yaml) | [![Image Size](https://ghcr-badge.egpl.dev/sredevopsorg/ghost-on-kubernetes/size?color=%2344cc11&tag=main&label=main+image+size)](https://github.com/sredevopsorg/ghost-on-kubernetes/pkgs/container/ghost-on-kubernetes) | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/sredevopsorg/ghost-on-kubernetes/badge)](https://securityscorecards.dev/viewer/?uri=github.com/sredevopsorg/ghost-on-kubernetes) | ![Fork this repository](https://img.shields.io/github/forks/sredevopsorg/ghost-on-kubernetes?style=social) | ![Star this repository](https://img.shields.io/github/stars/sredevopsorg/ghost-on-kubernetes?style=social) | [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8888/badge)](https://www.bestpractices.dev/projects/8888)
[![Build Multiarch](https://github.com/sredevopsorg/ghost-on-kubernetes/actions/workflows/multi-build.yaml/badge.svg?branch=main)](https://github.com/sredevopsorg/ghost-on-kubernetes/actions/workflows/multi-build.yaml) | [![Image Size](https://ghcr-badge.egpl.dev/sredevopsorg/ghost-on-kubernetes/size?color=%2344cc11&tag=main&label=main+image+size)](https://github.com/sredevopsorg/ghost-on-kubernetes/pkgs/container/ghost-on-kubernetes) | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/sredevopsorg/ghost-on-kubernetes/badge)](https://securityscorecards.dev/viewer/?uri=github.com/sredevopsorg/ghost-on-kubernetes) | [![Fork this repository](https://img.shields.io/github/forks/sredevopsorg/ghost-on-kubernetes?style=social)](https://github.com/sredevopsorg/ghost-on-kubernetes/fork) | [![Star this repository](https://img.shields.io/github/stars/sredevopsorg/ghost-on-kubernetes?style=social)](https://github.com/sredevopsorg/ghost-on-kubernetes/stargazers) | [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8888/badge)](https://www.bestpractices.dev/projects/8888)

> This repository implements Ghost CMS v5.xx.x from [@TryGhost (upstream)](https://github.com/TryGhost/Ghost) on Kubernetes, with our custom image, which has significant improvements to be used on Kubernetes. See this whole README for more information.

Expand All @@ -23,8 +23,129 @@ We've made some significant updates to improve the security and efficiency of ou
1. **Multi-arch support**: The images are now multi-arch, with [support for amd64 and arm64](#arm64-compatible).
2. **Distroless Image**: We use [@GoogleContainerTools](https://github.com/GoogleContainerTools)'s [Distroless NodeJS](https://github.com/GoogleContainerTools/distroless/blob/main/examples/nodejs/Dockerfile) 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.
3. **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.
4. **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 1000:1000. Check [deploy/06-ghost-deployment.yaml](https://github.com/sredevopsorg/ghost-on-kubernetes/blob/main/deploy/06-ghost-deployment.yaml) for details on these changes.
5. **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 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. [entrypoint.js](https://github.com/sredevopsorg/ghost-on-kubernetes/blob/main/entrypoint.js)
4. **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](https://github.com/sredevopsorg/ghost-on-kubernetes/blob/main/deploy/06-ghost-deployment.yaml) for details on these changes.
5. **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](https://github.com/sredevopsorg/ghost-on-kubernetes/blob/main/entrypoint.js)

```Dockerfile
#
# 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"]
```


## Features

Expand Down
8 changes: 4 additions & 4 deletions deploy/02-pvc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ metadata:
app.kubernetes.io/part-of: ghost-on-kubernetes
app.kubernetes.io/managed-by: sredevopsorg
spec:
storageClassName: longhorn-rwx # Change this to your storageClassName
storageClassName: local-path # Change this to your storageClassName
volumeMode: Filesystem
accessModes:
- ReadWriteMany # Change this to your accessModes if needed, we suggest ReadWriteMany so we can scale the deployment later.
- ReadWriteOnce # Change this to your accessModes if needed, we suggest ReadWriteOnce so we can scale the deployment later.
resources:
requests:
storage: 1Gi
Expand All @@ -35,10 +35,10 @@ metadata:
app.kubernetes.io/managed-by: sredevopsorg

spec:
storageClassName: longhorn-rwx # Change this to your storageClassName
storageClassName: local-path # Change this to your storageClassName
volumeMode: Filesystem
accessModes:
- ReadWriteMany # Change this to ReadWriteOnce if your storageClassName does not support ReadWriteMany
- ReadWriteOnce # Change this to ReadWriteOnce if your storageClassName does not support ReadWriteOnce
resources:
requests:
storage: 1Gi
4 changes: 2 additions & 2 deletions deploy/04-config.development.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ stringData:
"database": {
"client": "sqlite3",
"connection": {
"filename": "/var/lib/ghost/content/data/ghost-dev.db"
"filename": "/home/nonroot/app/ghost/content/data/ghost-dev.db"
}
},
"debug": true,
"process": "local",
"paths": {
"contentPath": "/var/lib/ghost/content"
"contentPath": "/home/nonroot/app/ghost/content"
},
"privacy": {
"useUpdateCheck": false,
Expand Down
Loading