Skip to content
Runner Images

Runner Images

ephemerd uses OCI container images to define the execution environment for each job. The image determines what tools, runtimes, and system packages are available during the workflow run.

GitHub Actions

How it works

GitHub Actions jobs run inside a single container. The runner binary lives inside the image, and job steps execute in the same container. ephemerd pulls the image, starts a container, and the embedded runner picks up the job.

Default images

PlatformDefault image
Linuxghcr.io/actions/actions-runner:latest
Windowsmcr.microsoft.com/windows/servercore:ltsc20XX (auto-detected)

Specifying an image

Use the container: key in your workflow YAML:

jobs:
  build:
    runs-on: [self-hosted, linux, x64]
    container: ghcr.io/your-org/ci-image:latest
    steps:
      - uses: actions/checkout@v4
      - run: make test

Building custom images

Custom images must extend the upstream GitHub Actions runner base image. This is important – the base includes the runner binary that ephemerd needs to execute jobs.

Linux:

FROM ghcr.io/actions/actions-runner:latest

USER root

RUN apt-get update && apt-get install -y \
    build-essential cmake autoconf automake \
    git curl wget pkg-config \
    && rm -rf /var/lib/apt/lists/*

# Add language runtimes, SDKs, etc.
# RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y

USER runner

For multi-arch builds (amd64 + arm64):

docker buildx build --platform linux/amd64,linux/arm64 \
    -t ghcr.io/your-org/ci-image:latest --push .

Windows:

# escape=`
FROM ghcr.io/actions/actions-runner:latest-win

SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop';"]

RUN Invoke-WebRequest -Uri "https://go.dev/dl/go1.26.1.windows-amd64.zip" -OutFile go.zip; `
    Expand-Archive go.zip -DestinationPath C:\; `
    Remove-Item go.zip
ENV PATH="C:\go\bin;${PATH}"

Windows images must be built on a Windows host.

macOS (artifact image)

macOS jobs run in per-job VMs, not containers. Set EPHEMERD_IMAGE in your workflow to deliver pre-built tools via an OCI artifact image:

jobs:
  build:
    runs-on: [self-hosted, macos]
    env:
      EPHEMERD_IMAGE: ghcr.io/your-org/macos-xcode16:latest
    steps:
      - run: xcodebuild -version

The image is a FROM scratch container with binaries copied from a builder stage. ephemerd pulls it, extracts the layers, and mounts them into the macOS VM via virtio-fs:

FROM golang:1.26-bookworm AS builder
RUN GOOS=darwin GOARCH=arm64 go build -o /deps/bin/mage github.com/magefile/mage

FROM scratch
COPY --from=builder /deps /deps

Forgejo / Gitea

There are two ways to run Forgejo/Gitea jobs, each with different image requirements.

Option 1: ephemerd-runner-forgejo (single container)

ephemerd-runner-forgejo runs inside a single container alongside the workflow steps. ephemerd mounts the ephemerd-runner-forgejo binary into the container — the image just needs CI tools.

    flowchart LR
    E["ephemerd"] -->|containerd create| C["Single Container\nephemerd-runner-forgejo + CI tools"]
    C -->|os/exec| S["workflow steps"]
    style C fill:#e1f5ff,stroke:#0288d1
    style S fill:#fff3e0,stroke:#f57c00
  

The default image is gitea/runner-images:ubuntu-24.04. Customize it by adding your build dependencies:

FROM gitea/runner-images:ubuntu-24.04

RUN apt-get update && apt-get install -y \
    build-essential cmake pkg-config \
    && rm -rf /var/lib/apt/lists/*

Option 2: upstream runner + fake Docker socket (two containers)

The upstream forgejo-runner / act_runner creates a separate job container via the Docker API. Two images are involved:

ImagePurposeConfig key
Runner imageContains the runner daemon binary[runner] default_image
Job imageWhere workflow steps execute[forgejo] job_image or [gitea] job_image
    flowchart LR
    RC["Runner Container\nforgejo-runner"] -->|Docker API| DS["Fake Docker Socket\npkg/dind"]
    DS -->|containerd create| JC["Job Container\nubuntu-24.04"]
    style DS fill:#f3e5f5,stroke:#7b1fa2
    style RC fill:#e1f5ff,stroke:#0288d1
    style JC fill:#fff3e0,stroke:#f57c00
  

Customize the job image the same way. The runner image rarely needs customization — the upstream images work out of the box.

Config (both options)

[forgejo]
instance_url = "https://codeberg.org"
token = "runner-registration-token"
owner = "your-org"
job_image = "ghcr.io/your-org/ci-job:latest"

GitLab

How it works

GitLab uses a custom executor model. The gitlab-runner binary drives the job lifecycle and calls ephemerd scripts for each phase: prepare (create container), run (execute steps), cleanup (destroy container). ephemerd doesn’t discover jobs – gitlab-runner polls GitLab and delegates to ephemerd.

Images

The job image comes from the image: field in .gitlab-ci.yml – it’s part of the job payload, so no extra API call is needed. You don’t configure a default image in ephemerd; GitLab handles image selection.

# .gitlab-ci.yml
build:
  image: ghcr.io/your-org/ci-image:latest
  script:
    - make test

Any Docker image works. The gitlab-runner custom executor creates the container via ephemerd, which uses containerd to pull and run it.

Woodpecker CI

How it works

Woodpecker uses an agent model. The Woodpecker agent connects to the server via gRPC, receives pipeline definitions, and creates containers for each step. ephemerd manages the agent lifecycle – it runs the agent binary inside a container, and the agent creates step containers via the Docker API (intercepted by ephemerd’s fake Docker socket, same as Forgejo/Gitea).

Images

Pipeline step images are defined in .woodpecker.yml:

# .woodpecker.yml
steps:
  - name: build
    image: ghcr.io/your-org/ci-image:latest
    commands:
      - make test

The agent pulls step images via the fake Docker socket. Any OCI image works. There’s no separate “runner image” to configure – the Woodpecker agent image is managed by ephemerd internally.

Per-Repo Image Overrides

Override the default image for specific repositories in the config:

[runner]
default_image = "ghcr.io/your-org/ci-image:latest"

[runner.repo_images]
"my-go-project" = "ghcr.io/your-org/go-ci:latest"
"my-rust-project" = "ghcr.io/your-org/rust-ci:latest"

One Image, Every Host

The same Linux container image runs identically on Linux, Windows (via WSL2), and macOS (via Virtualization.framework). In all three cases, containerd is the runtime that pulls and executes the image. There is no need to maintain separate images per host platform.

Reference: ephemerd CI Images

ephemerd’s own CI uses custom runner images that pre-cache all build dependencies. These live in the images/ directory and serve as a real-world example:

ImageBaseWhat it caches
runner-ci-linuxghcr.io/actions/actions-runner:latestGo, Mage, runner archive, CNI plugins, containerd shim, runc, golangci-lint
runner-ci-windowsghcr.io/actions/actions-runner:latest-winGo, Mage, runner archive (Windows + Linux), golangci-lint
runner-ci-macosscratchRunner archive (macOS), Mage, golangci-lint (cross-compiled for darwin)

The Linux image supports multi-arch (amd64 + arm64) via docker buildx. Each image includes an entrypoint script that copies the cached dependencies into the workspace so mage ci runs without downloading anything. The Go module cache is also enabled – after the first CI job runs, the module cache is warm and all subsequent jobs skip the go mod download entirely. The first job downloads and builds everything; every job after that just copies in the cached assets and runs mage ci.