Static web hosting on Kubernetes with OCI images as volumes - kowalski7ccStatic web hosting on Kubernetes with OCI images as volumes
All posts
Sticker by @puzzoz
Introduced in Kubernetes 1.31, promoted in beta in 1.33 and entered in stable with Kubernetes 1.36, we can use OCI images as volumes for containers. They are now a possible storage choice among Persistent Volume Claims, empty directory, pod metadata, Config Maps and Secrets. This option can take advantage of the existing tooling and CI/CD already present to deriver an archive containing any kind of data.
OCI images, the evoultion of Docker images, have become a new way to deliver artifacts, to the point that they even are also used by some Linux distribuson as a way to build and ship system updates.
By leveraging OCI images with this new feature we can improve our static website developement and deployment by decoupling webserver image and static website build, simplifying application versioning and making quicker web server patching without rebuilding everything.
Creating website inages, the old way
Until now, the standard way to host a website on Kubernetes was, after eventually building the site assets, to extend a web server image (in this case Nginx) with the files of the static website copied in the www root folder.<br>An example Containerfile for building my website with this approch could be the following:
FROM registry.access.redhat.com/ubi10/nodejs-24-minimal:10.1 as build<br>WORKDIR $HOME<br>ADD package*.json ./<br>RUN --mount=type=cache,mode=0777,uid=1001,gid=0,target=node_modules \<br>npm ci --only=production<br>ADD . .<br>RUN --mount=type=cache,mode=0777,uid=1001,gid=0,target=node_modules \<br>--mount=type=cache,mode=0777,uid=1001,gid=0,target=public \<br>--mount=type=cache,mode=0777,uid=1001,gid=0,target=.cache \<br>npm run build
FROM registry.access.redhat.com/ubi10/nginx-126:10.1<br>CMD nginx -g "daemon off;"<br>COPY --from=build $HOME/public $HOME
With the new approach, we can replace the Nginx base image with scratch that is a empty base image, opposed to a distro or runtime base image developer use in a typical scenario.
FROM registry.access.redhat.com/ubi10/nodejs-24-minimal:10.1 as build<br>WORKDIR $HOME<br>ADD package*.json ./<br>RUN --mount=type=cache,mode=0777,uid=1001,gid=0,target=node_modules \<br>npm ci --only=production<br>ADD . .<br>RUN --mount=type=cache,mode=0777,uid=1001,gid=0,target=node_modules \<br>--mount=type=cache,mode=0777,uid=1001,gid=0,target=.cache \<br>npm run build
FROM scratch<br>COPY --from=build /opt/app-root/src /
Let's see with podman images what are the differences between the two images:
REPOSITORY TAG IMAGE ID CREATED SIZE<br>ghcr.io/kowalski7cc/website scratch f1e26dd79993 13 seconds ago 629 MB<br>ghcr.io/kowalski7cc/website nginx a108d5cebad5 4 minutes ago 428 MB
We can see the image kowalski7cc/website:scratch is taking significanly less space than the kowalski7cc/website:nginx image without changing anything about the website!
At this point we can push our smaller imagea and prepare a kubernetes deployment to it.
The basic syntax to use a OCI image as a volume is the following:
...<br>spec:<br>containers:<br>- volumeMounts:<br>- name: volume<br>mountPath: /volume<br>...<br>volumes:<br>- name: volume<br>image:<br>reference: $VOLUME_IMAGE<br>pullPolicy: IfNotPresent<br>...
So we can create a deployment like this example for a Nginx container, where we mount the ghcr.io/kowalski7cc/website:scratch image under the /opt/app-root/src, which is the default path used by Red Hat's Nginx image named registry.access.redhat.com/ubi10/nginx-126:10.1. With this image, is required to override the default image command with ["nginx", "-g", "daemon off;"], otherwise Nginx would start and instead we would find OCP s2i stub script.
apiVersion: apps/v1<br>kind: Deployment<br>metadata:<br>labels:<br>app: website<br>name: website<br>namespace: website<br>spec:<br>selector:<br>matchLabels:<br>app: website<br>template:<br>metadata:<br>labels:<br>app: website<br>namespace: website<br>spec:<br>containers:<br>- image: registry.access.redhat.com/ubi10/nginx-126:10.1<br>imagePullPolicy: IfNotPresent<br>name: website<br>command:<br>- nginx<br>- -g<br>- daemon off;<br>ports:<br>- containerPort: 8080<br>name: http<br>protocol: TCP<br>volumeMounts:<br>- mountPath: /opt/app-root/src<br>name: html<br>volumes:<br>- image:<br>pullPolicy: Always<br>reference: ghcr.io/kowalski7cc/website:scratch<br>name: html
Automatizing static site build with Actions
Once we have deployed our first image, we can automatize the build with any kind of pipeline. If you are using a forge compatible with GitHub Actions, such as Gitea or Forgejo, you can use the Action. In this example I'm using Gatsbyjs fraework for my website, but you can use your preferred one. After static site build, I use Red Hat buildah step to copy in a scratch image the static assets and publish them on my registry.
name: Build and push
on:<br>push:<br>branches: ["master"]
env:<br>REGISTRY: my-private-registry<br>IMAGE_NAME: ${{ github.repository }}<br>NODE: 22
jobs:<br>build:<br>runs-on: ubuntu-latest
steps:<br>- uses: actions/checkout@v6
- name: Use Node.js ${{ env.NODE }}<br>uses:...