Building Docker images with Forgejo Actions

Lately I was looking for a Docker registry that I could host locally and keep couple of images that I run on multiple hosts. I didn’t want to run a bare registry but something more complete and I settled on a Forgejo instance which will also make me able to keep Dockerfiles and use it’s integrated CI to build the images.

Forgejo’s CI is based on GitHub Actions (unfortunately - I’d love to see an option to use GitLab CI workflow and syntax instead) so it’s easy to find workflows that can be adapted for your needs. The setup of a runner is not that straightforward, though - Forgejo’s docs are a bit vague so I’ll present a minimal configuration that I got to work properly. For security reasons, I chose to build images with buildah since kaniko is no longer maintained .

I deployed my runner with Docker Compose on an Alpine Linux host:

volumes:
  runner-data:

services:
  forgejo-runner:
    image: data.forgejo.org/forgejo/runner:13
    container_name: forgejo-runner
    restart: unless-stopped
    command: forgejo-runner daemon --config /data/runner-config.yml
    group_add:
      - "102" # docker socket host group, obtained with "stat -c '%g' /var/run/docker.sock"
    volumes:
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - runner-data:/data
      - ./runner-config.yml:/data/runner-config.yml
      - ./seccomp.json:/data/seccomp.json:ro
      - /var/run/docker.sock:/var/run/docker.sock

Tricky part is to set proper group in group_add and mounting a seccomp.json file. The latter contains a default seccomp file (which you can get from https://github.com/moby/profiles/blob/main/seccomp/default.json ) with a set of extra rules added to syscalls dict:

{
    "names": ["unshare"],
    "action": "SCMP_ACT_ALLOW",
    "comment": "Allow unshare for buildah"
},
{
    "names": ["mount"],
    "action": "SCMP_ACT_ALLOW",
    "comment": "Allow mount for buildah"
},
{
    "names": ["umount2"],
    "action": "SCMP_ACT_ALLOW",
    "comment": "Allow umount2 for buildah"
}

I found these rules on David Robillard ’s blog - there’s no way I would come up with them on my own, at least not without extensive digging into seccomp.

The last piece of configuration needed is loading this modified seccomp profile in Forgejo runner’s config:

container:
  options:
    "--security-opt seccomp=/data/seccomp.json"

David’s post also mentions modifying AppArmor profile, but my Alpine host does not have AppArmor installed.

After applying the above configuration, a working workflow looks like this one:

name: build

on:
  push:
    branches:
      - master
  schedule:
    - cron: "0 8 * * 0" # each monday
      timezone: Europe/Warsaw

jobs:
  build:
    runs-on: docker
    container:
      image: git.example.com/adam/buildah-node:latest
    env:
      STORAGE_DRIVER: vfs
      IMAGE_REGISTRY: git.example.com
      IMAGE_NAME: adam/homelab-traefik
      IMAGE_TAG: v3
    steps:
      - name: Checkout
        uses: actions/checkout@v7

      - name: Login to Forgejo Container Registry
        run: echo "${{ secrets.TOKEN }}" | buildah login -u "${{ forgejo.actor }}" --password-stdin "${IMAGE_REGISTRY}"

      - name: Build and push
        run: |
          buildah bud -t "${IMAGE_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" .
          buildah push "${IMAGE_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"

For building, I use a custom buildah-node image that is based on official Buildah image with nodejs, git and my custom CA cert installed on top:

FROM quay.io/buildah/stable
RUN dnf install -y nodejs git && dnf clean all
COPY my-ca.crt /etc/pki/ca-trust/source/anchors/
RUN update-ca-trust

Keep in mind that this is a minimal configuration and probably can be additionally hardened. My Forgejo installation is not exposed to the world and I do not intend to use unverified CI workflows so this is good enough for me.