2026 Blog CI build updates
I have not touched the CI / CD pipeline in Gitlab that deploys this blog in several years. It was set up with a project that is now deprecated: Kaniko. The project built a Dockerfile (container image) without requiring root access or the docker socket mounted somewhere to build a container. In the years since I set up this blog, other projects have become available to build containers. I ended up switching to the buildkit project which I believe is directly from the company behind Docker.
GitLab CI/CD is a feature of the GitLab platform that is used in software development for Continuous Integration, Delivery, and Deployment of software. The idea is that software source code will grow and evolve over time but you do not want to have to manually build/compile and test your code each time a change is made. This is especially true when you develop software as a team and multiple changes might be made in a single day.
Gitlab Pipeline
My new pipeline also does not require root access or mounting the Docker socket. I am still using Gitlab to host the source code for my blog and as a CI/CD system to build and deploy my blog as a container image. My blog is a static site generated by Hugo. I create markdown files for blog posts and I created a theme that customizes the style of the website. Hugo takes these as input and generates a website of static HTML, CSS, and JS files that can be hosted on any web server.
For the rest of this post I will outline the two files that are needed to build the container and use Gitlab CI.
The pipeline is configured in .gitlab-ci.yml. Here is the file at the time of the post:
---
stages:
- build
buildTesting:
stage: build
image:
name: moby/buildkit:rootless
entrypoint: [""]
variables:
GIT_SUBMODULE_STRATEGY: recursive
BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
before_script:
- mkdir -p ~/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > ~/.docker/config.json
script:
- |
buildctl-daemonless.sh build \
--frontend dockerfile.v0 \
--local context="$CI_PROJECT_DIR" \
--local dockerfile="$CI_PROJECT_DIR" \
--output type=image,name=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA,push=true
rules:
- if: '$CI_COMMIT_BRANCH == "testing"'
buildProduction:
stage: build
image:
name: moby/buildkit:rootless
entrypoint: [""]
variables:
GIT_SUBMODULE_STRATEGY: recursive
BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
before_script:
- mkdir -p ~/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > ~/.docker/config.json
script:
- |
buildctl-daemonless.sh build \
--frontend dockerfile.v0 \
--local context="$CI_PROJECT_DIR" \
--local dockerfile="$CI_PROJECT_DIR" \
--output type=image,name=$CI_REGISTRY_IMAGE:latest,push=true
rules:
- if: '$CI_COMMIT_BRANCH == "main"'Pipeline Structure
The CI pipeline has one stage:
- build: Creates Docker images using BuildKit
Jobs
buildTesting
- Trigger: Runs on commits to the
testingbranch - Purpose: Builds and pushes a Docker image tagged with the commit SHA
- Image tag:
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
buildProduction
- Trigger: Runs on commits to the
mainbranch - Purpose: Builds and pushes a Docker image tagged as latest
- Image tag:
$CI_REGISTRY_IMAGE:latest
Build Process
Both jobs use the same build process:
- Image: Uses
moby/buildkit:rootlessfor secure, daemonless Docker builds - Submodules: Recursively clones git submodules (required for the Hugo theme)
- Authentication: Configures Docker registry credentials using GitLab CI variables
- Build: Uses BuildKit’s
buildctl-daemonless.shto build the Dockerfile - Push: Automatically pushes the built image to the GitLab Container Registry
Environment Variables
The pipeline uses these GitLab CI built-in variables:
CI_REGISTRY: GitLab Container Registry URLCI_REGISTRY_USER: Registry username (automatically provided)CI_REGISTRY_PASSWORD: Registry password (automatically provided)CI_REGISTRY_IMAGE: Full image name including registry pathCI_COMMIT_SHA: Git commit hash for tagging testing buildsCI_PROJECT_DIR: Project directory path
Docker Build
Here is the Dockerfile I am using at the time of this post:
# syntax=docker/dockerfile:1.4
# Use Debian slim image with Go and necessary tools
FROM debian:bookworm-slim AS hugo
# Install Hugo extended dependencies
RUN apt-get update && apt-get install -y \
curl \
git \
ca-certificates \
libsass-dev \
&& rm -rf /var/lib/apt/lists/*
# Download Hugo Extended binary
ENV HUGO_VERSION=0.153.3
RUN curl -L -o hugo.tar.gz https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz \
&& tar -xzf hugo.tar.gz \
&& mv hugo /usr/local/bin/hugo \
&& chmod +x /usr/local/bin/hugo
WORKDIR /site
COPY . .
# Build the Hugo site
RUN hugo --minify
# Final stateless NGINX image
FROM nginx:alpine
COPY --from=hugo /site/public /usr/share/nginx/htmlThe Dockerfile creates a multi-stage build:
- Hugo stage: Downloads Hugo Extended, builds the static site
- NGINX stage: Serves the built site using NGINX Alpine
Deployment
After the pipeline completes:
- Testing builds: Available as
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - Production builds: Available as
$CI_REGISTRY_IMAGE:latest
Since the project is public, anyone can pull and run these images from the project’s GitLab Container Registry.
The container image that is built will start an nginx web server that serves the static pages generated by Hugo in the pipeline.
New disclaimer: I used an LLM to help create this post. Opinions expressed are likely from me and not the LLM.