← Back to posts
comparison guide self-hosted · · 12 min read

GitHub Actions Runner vs GitLab Runner vs Woodpecker: Self-Hosted CI Agents 2026

Compare GitHub Actions Runner, GitLab Runner, and Woodpecker CI for self-hosted CI/CD execution. Docker setup, architecture differences, and deployment guides.

OS
Editorial Team

Running CI/CD pipelines on shared cloud infrastructure means your build logs, source code, and artifacts pass through servers you don’t control. For teams handling proprietary code, compliance-sensitive workloads, or simply wanting faster builds on local hardware, self-hosted CI runners are the answer. This guide compares the three most popular self-hosted CI execution agents: GitHub Actions Runner, GitLab Runner, and Woodpecker CI — covering architecture, docker deployment, configuration, and when to choose each one.

Why Self-Host Your CI Runner?

Cloud-hosted CI is convenient but comes with trade-offs:

  • Cost at scale: Shared runners charge per-minute. Running hundreds of builds monthly adds up quickly compared to using your own hardware.
  • Security and compliance: Source code and build artifacts never leave your infrastructure. Critical for HIPAA, SOC 2, or internal security policies.
  • Hardware control: Need GPUs for ML model training, ARM64 for cross-compilation, or 128 GB RAM for large builds? Self-hosted runners give you full hardware access.
  • Network speed: Local runners pull dependencies from your internal artifact registry at LAN speeds instead of downloading over the internet.
  • Custom environments: Pre-install proprietary SDKs, internal tooling, or licensed software that cloud runners can’t access.

The runner is the component that actually executes your pipeline steps. It connects to your CI platform, pulls job definitions, runs commands, and reports results back. Choosing the right runner affects everything from build speed to security posture.

Project Overview

FeatureGitHub Actions RunnerGitLab RunnerWoodpecker CI
Repositoryactions/runnergitlab-org/gitlab-runnerwoodpecker-ci/woodpecker
Stars5,9542,5516,834
LanguageC#GoGo
Last Updated2026-04-172026-04-182026-04-18
LicenseMITMITApache 2.0
Docker SupportNative container actionsDocker executor, Docker-in-DockerDocker-native (container-per-step)
Executor TypesProcess, ContainekubernetesShell, Docker, Kubernetes, Parallels, CustomDocker, Kubernetes, Local, SSH
Platform Lock-inGitHub onlyGitLab onlyPlatform-agnostic
ScalingAuto-scaling via actions-runner-controllerAuto-scaling via Docker Machine / KubernetesHorizontal via additional agents
Ephemeral JobsYes (container actions)Yes (Docker executor)Yes (default model)

GitHub Actions Runner is Microsoft’s official execution agent for GitHub Actions. It connects to your repository or organization and runs jobs defined in .github/workflows/. The runner itself is open source (MIT), though the Actions platform is proprietary. It supports both self-hosted Linux/Windows/macOS machines and container-based execution.

GitLab Runner is the execution agent for GitLab CI/CD. Written in Go, it’s lightweight and supports multiple executor backends including Docker, Kubernetes, Shell, and SSH. It can run jobs for GitLab.com (SaaS) or self-hosted GitLab instances. The auto-scaling feature via Docker Machine or Kubernetes makes it suitable for high-volume CI environments.

Woodpecker CI takes a different approach — it’s a complete CI/CD engine (server + agent) that’s fully open source and platform-agnostic. Rather than being tied to a specific Ggitearge, Woodpecker integrates with GitHub, GitLab, Gitea, Forgejo, and Bitbucket through OAuth. Its architecture is container-native: every pipeline step runs in its own isolated Docker container, making it ideal for teams that want a clean, self-contained CI system.

Installation and Configuration

GitHub Actions Runner

The GitHub Actions Runner can be deployed on any Linux, Windows, or macOS machine. Here’s the Docker-based deployment for a Linux runner:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Create runner directory
mkdir -p /opt/github-runner && cd /opt/github-runner

# Pull and run the runner container
docker run -d --restart always \
  --name github-runner \
  -e REPO_URL="https://github.com/your-org/your-repo" \
  -e RUNNER_NAME="self-hosted-runner-01" \
  -e RUNNER_TOKEN="<registration-token>" \
  -e RUNNER_WORKDIR="/opt/github-runner/_work" \
  -v /opt/github-runner/_work:/opt/github-runner/_work \
  -v /var/run/docker.sock:/var/run/docker.sock \
  myoung34/github-runner:latest

You can also deploy at scale using the official actions-runner-controller for Kubernetes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: github-runner
spec:
  replicas: 3
  template:
    spec:
      repository: your-org/your-repo
      dockerEnabled: true
      labels: ["self-hosted", "linux", "x64"]
      resources:
        limits:
          cpu: "2"
          memory: "4Gi"

Register a runner manually on a bare-metal machine:

1
2
3
4
5
6
7
# Download the runner
curl -o actions-runner-linux-x64.tar.gz -L \
  https://github.com/actions/runner/releases/download/v2.320.0/actions-runner-linux-x64-2.320.0.tar.gz

tar xzf actions-runner-linux-x64.tar.gz
./config.sh --url https://github.com/your-org/your-repo --token <token> --unattended
./run.sh

Runner configuration is stored in .runner files within the runner directory. Each runner has labels that determine which workflow jobs it picks up:

1
2
3
4
5
6
7
8
# .github/workflows/build.yml
jobs:
  build:
    runs-on: [self-hosted, linux, x64, gpu]
    steps:
      - uses: actions/checkout@v4
      - name: Build with GPU
        run: docker run --gpus all nvidia/cuda:12.0 ./build.sh

GitLab Runner

GitLab Runner supports multiple executor modes. The Docker executor is the most common for self-hosted setups:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# config.toml - GitLab Runner configuration
concurrent = 4
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "docker-runner-01"
  url = "https://gitlab.example.com/"
  id = 1
  token = "<runner-registration-token>"
  executor = "docker"
  [runners.custom_build_dir]
    enabled = true
  [runners.docker]
    tls_verify = false
    image = "alpine:latest"
    privileged = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"]
    shm_size = 0
    pull_policy = ["if-not-present"]
  [runners.cache]
    Type = "s3"
    Path = "gitlab-runner-cache"
    [runners.cache.s3]
      ServerAddress = "s3.example.com"
      BucketName = "runner-cache"
      BucketLocation = "us-east-1"

Deploy with Docker Compose:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
version: "3.8"
services:
  gitlab-runner:
    image: gitlab/gitlab-runner:latest
    container_name: gitlab-runner
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./config:/etc/gitlab-runner
      - ./cache:/cache
    environment:
      - CI_SERVER_URL=https://gitlab.example.com/
      - REGISTRATION_TOKEN=<your-token>
      - RUNNER_NAME=docker-runner-01
      - RUNNER_EXECUTOR=docker
      - RUNNER_TAG_LIST=docker,linux,self-hosted

Register a runner programmatically:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
docker exec -it gitlab-runner gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.example.com/" \
  --registration-token "<token>" \
  --executor "docker" \
  --docker-image "alpine:latest" \
  --description "docker-runner-01" \
  --tag-list "docker,linux,self-hosted" \
  --run-untagged="true" \
  --locked="false"

GitLab CI pipeline configuration uses .gitlab-ci.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
stages:
  - build
  - test
  - deploy

build:
  stage: build
  tags: [self-hosted, docker]
  image: node:20-alpine
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

test:
  stage: test
  tags: [self-hosted, docker]
  image: node:20-alpine
  services:
    - postgres:15-alpine
  variables:
    POSTGRES_DB: testdb
    POSTGRES_USER: test
    POSTGRES_PASSWORD: test
  script:
    - npm ci
    - npm test

Woodpecker CI

Woodpecker CI has a server-and-agent architecture. The server manages the UI, webhooks, and scheduling; agents execute the actual pipeline steps.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# docker-compose.yml for Woodpecker CI
services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:latest
    container_name: woodpecker-server
    ports:
      - "8080:8080"
      - "9000:9000"
    volumes:
      - woodpecker-data:/var/lib/woodpecker
    environment:
      - WOODPECKER_OPEN=true
      - WOODPECKER_HOST=https://ci.example.com
      - WOODPECKER_GITHUB=true
      - WOODPECKER_GITHUB_CLIENT=your-client-id
      - WOODPECKER_GITHUB_SECRET=your-client-secret
      - WOODPECKER_AGENT_SECRET=your-agent-secret
    restart: always

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:latest
    container_name: woodpecker-agent
    command: agent
    restart: always
    depends_on:
      - woodpecker-server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=your-agent-secret
      - WOODPECKER_MAX_WORKFLOWS=4

volumes:
  woodpecker-data:

Woodpecker pipeline configuration uses .woodpecker.yml in the repository root:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
steps:
  build:
    image: node:20-alpine
    commands:
      - npm ci
      - npm run build

  test:
    image: node:20-alpine
    commands:
      - npm ci
      - npm test
    when:
      branch: [main, develop]

  deploy:
    image: plugins/docker
    settings:
      repo: registry.example.com/myapp
      tags: latest
      registry: registry.example.com
    when:
      event: push
      branch: main

Scale agents by adding more agent containers behind the same server:

1
2
3
4
5
6
7
8
9
# Deploy additional agents for parallel execution
for i in 2 3 4; do
  docker run -d --name woodpecker-agent-$i \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -e WOODPECKER_SERVER=woodpecker-server:9000 \
    -e WOODPECKER_AGENT_SECRET=your-agent-secret \
    -e WOODPECKER_MAX_WORKFLOWS=4 \
    woodpeckerci/woodpecker-agent:latest
done

Architecture Comparison

Execution Model

GitHub Actions Runner uses a job-based model where each job runs on a single runner. Jobs can use container actions (Docker-based) or run directly on the host (process executor). The runner maintains a persistent connection to GitHub’s servers via WebSocket, pulling job assignments as they become available. Multiple runners can be organized into pools with label-based routing.

GitLab Runner operates similarly but with more executor flexibility. Each runner instance can handle multiple concurrent jobs (controlled by the concurrent setting). The Docker executor spawns a new container per job, providing isolation. The Kubernetes executor creates pods on demand. GitLab Runner also supports the services directive, which spins up additional containers (databases, caches) alongside the main job container.

Woodpecker CI uses a pipeline-as-containers model. Every step in a pipeline runs in its own Docker container, and steps execute sequentially by default. Parallel execution is possible using the depends_on directive. The architecture is simpler by design — no concept of “executors” since Docker is the default and primary execution backend. This makes Woodpecker easier to reason about but less flexible for non-container workloads.

Security Model

AspectGitHub Actions RunnerGitLab RunnerWoodpecker CI
Runner RegistrationTime-limited registration tokensRegistration tokens or JWTShared agent secret
Job IsolationContainer or process-levelDocker container / Kubernetes podDocker container per step
Secrets StorageGitHub Encrypted SecretsGitLab CI/CD VariablesWoodpecker Secrets (per-repo)
Network AccessRunner has full outbound accessRunner outbound; services internalAgent-to-server only
Privileged ModeSupported in container actionsConfigurable per runnerNot available by default
Audit LoggingGitHub audit log + runner diagnostic logsRunner logs + GitLab audit eventsServer logs + agent logs

For compliance-sensitive environments, GitLab Runner offers the most granular control — you can configure TLS verification, set specific Docker networks, restrict which images are allowed, and enable job-level logging rotation. GitHub Actions Runner benefits from GitHub’s enterprise audit infrastructure but gives you less control over the runner itself. Woodpecker CI’s simplicity is a security advantage — fewer moving parts means a smaller attack surface.

Performance and Scaling

When evaluating runners for production use, consider these factors:

  • Startup time: Woodpecker’s container-per-step model means each step pulls its image fresh unless cached. GitLab Runner with Docker executor caches images between jobs. GitHub Actions Runner reuses the host filesystem between jobs, giving the fastest warm-start times.

  • Concurrent execution: GitLab Runner’s concurrent setting controls how many jobs run simultaneously on a single instance. Woodpecker scales horizontally by adding more agent instances. GitHub Actions Runner scales by adding more runner instances managed through labels or the Kubernetes controller.

  • Caching strategies: GitLab Runner supports S3, GCS, and Azure cache backends for shared caching across runners. GitHub Actions provides a cache action with configurable keys. Woodpecker relies on Docker volume caching and the restore_cache / rebuild_cache pipeline steps.

  • Resource limits: GitLab Runner lets you set CPU and memory limits per executor. GitHub Actions Runner inherits the host’s resource limits. Woodpecker agents can be constrained via Docker resource flags.

For teams running 50+ concurrent builds, GitLab Runner with Kubernetes executor provides the most robust auto-scaling. For smaller teams (5-20 concurrent builds), a few GitHub Actions Runners or Woodpecker agents on dedicated VMs work well.

Which Runner Should You Choose?

Choose GitHub Actions Runner if:

  • Your team already uses GitHub for version control
  • You want seamless integration with GitHub Actions ecosystem (Actions Marketplace)
  • You need Windows or macOS runners alongside Linux
  • You’re already invested in the GitHub ecosystem for issues, projects, and packages

Choose GitLab Runner if:

  • You run a self-hosted GitLab instance
  • You need Kubernetes-native CI execution
  • You want the most executor variety (Docker, SSH, Shell, Kubernetes, Parallels)
  • You need fine-grained caching with S3/GCS/Azure backends

Choose Woodpecker CI if:

  • You want a fully open-source, self-contained CI/CD system
  • You use Gitea or Forgejo as your Git forge
  • You prefer a simple, container-native architecture
  • You want platform independence — not locked into a single Git provider
  • You value a minimal resource footprint and straightforward configuration

For related reading, see our self-hosted CI/CD platforms comparison for a broader overview of CI engines, the container build tools guide for optimizing build steps within your runners, and the self-hosted Git forge comparison for choosing the version control platform that pairs with your runner.

FAQ

Can I run GitHub Actions Runner without an internet connection?

GitHub Actions Runner requires outbound connectivity to github.com to receive job assignments and report results. It cannot operate in a fully air-gapped environment. If you need fully offline CI, consider Woodpecker CI with a self-hosted Gitea instance, or GitLab Runner connected to an air-gapped GitLab instance.

How do I secure secrets in self-hosted runners?

All three runners support encrypted secrets that are injected as environment variables during job execution. GitHub Actions uses repository or organization-level encrypted secrets. GitLab Runner uses CI/CD variables with masking and protection rules. Woodpecker CI stores secrets per-repository on the server, transmitted to agents over an encrypted channel. Never log secrets in pipeline output — all three platforms support secret masking in build logs.

Can I mix cloud and self-hosted runners in the same project?

Yes. GitHub Actions lets you route specific jobs to runs-on: self-hosted while others use runs-on: ubuntu-latest. GitLab Runner uses tags — tag your self-hosted runners and reference them with tags: [self-hosted] in .gitlab-ci.yml. Jobs without matching tags will use shared runners if available. Woodpecker CI only runs on your self-hosted agents, but you can connect it to cloud Git repositories.

How many concurrent jobs can a single runner handle?

GitHub Actions Runner processes one job at a time per instance — scale by adding more runner instances. GitLab Runner can handle multiple concurrent jobs on a single instance, controlled by the concurrent setting in config.toml (default: 1). Woodpecker agents process workflows sequentially but support multiple steps in parallel within a single workflow using the depends_on directive. Scale Woodpecker by deploying additional agent containers.

Do self-hosted runners support Docker-in-Docker (DinD)?

Yes, all three support Docker-in-Docker workflows. GitHub Actions Runner exposes the Docker socket via the docker:// service container. GitLab Runner enables DinD with privileged = true in the Docker executor configuration and the docker:dind service. Woodpecker CI mounts the Docker socket into step containers, allowing build steps that create Docker images. For production DinD, consider using Kaniko (building without Docker daemon) for better security isolation.

What happens if a runner goes offline during a job?

GitHub Actions will mark the job as failed and queue it for retry if another runner is available. GitLab Runner has a configurable output_limit and job timeout — if a runner disconnects, the job is marked stuck and can be retried manually or automatically. Woodpecker CI detects agent disconnection via heartbeat and marks the pipeline as errored; the server can reschedule failed pipelines if configured with retry policies.

Advertise here