Configure Forgejo Actions with an Isolated Docker Runner
Set up a Forgejo Actions runner in Docker mode to keep CI workloads separate from the Forgejo server, improving security isolation.
05 Mar 2026, 13:26 UTC

Desired outcome
You will have a Forgejo instance that can run CI/CD workflows via a separate forgejo-runner process executing inside Docker containers. This isolates the runner’s filesystem and privileges from the main Forgejo server, reducing the risk that a compromised workflow could affect the host or the Forgejo application.
Prerequisites
- A running Forgejo instance (version 1.20 or later) accessible via HTTP or HTTPS.
- Administrative access to the Forgejo web UI to create an Actions runner token.
- A Linux host (or VM) where you can run Docker Engine (version 20.10 or newer). The host must have the
dockercommand available to the user that will runforgejo-runner. - The
forgejo-runnerbinary for your platform, downloaded from the Forgejo releases page. - Basic familiarity with YAML and the Forgejo Actions workflow syntax.
Procedure
-
Create a runner token in Forgejo
Log in to Forgejo as an administrator, go to Site Administration → Actions → Runners (or Repository Settings → Actions → Runners for a repo‑specific token). Click New Runner, choose a descriptive name (e.g.,
docker-runner-01), and optionally assign labels such asdockerandubuntu-latest. Copy the generated token; you will need it in the runner configuration. -
Install and configure
forgejo-runner- Download the binary matching your host architecture:
# Example for Linux x86_64 curl -L -o forgejo-runner https://forgejo.org/releases/runner/forgejo-runner-linux-amd64 chmod +x forgejo-runner mv forgejo-runner /usr/local/bin/- Create a configuration directory and file:
sudo mkdir -p /etc/forgejo-runner sudo touch /etc/forgejo-runner/config.yaml sudo chown $USER:$USER /etc/forgejo-runner/config.yaml- Edit
/etc/forgejo-runner/config.yamlwith the following content (replace placeholders):
# /etc/forgejo-runner/config.yaml log_level: "info" server: # URL of your Forgejo instance (no trailing slash) gitea_url: "https://forgejo.example.com" # The token you copied in step 1 token: "YOUR_RUNNER_TOKEN_HERE" # Labels must match those used in workflow YAML labels: ["docker", "ubuntu-latest"] # Docker‑based execution mode runner: # Use Docker container isolation docker: enabled: true # Image used as the base for job containers; adjust as needed image: "ubuntu:22.04" # Optional: allow privileged containers if your jobs need it (adds risk) privileged: false # Mount the Docker socket so the runner can launch containers # (requires the runner process to have access to /var/run/docker.sock) # See the "Permissions" note below. # No additional fields required for basic use. # Optional: limit concurrent jobs # concurrent: 2- Ensure the runner user can access the Docker socket. The simplest approach is to add the user to the
dockergroup:
sudo usermod -aG docker $USER newgrp docker # refresh group membership in current sessionRisk: Giving a user access to the Docker socket is equivalent to granting root privileges on the host. Only do this on a trusted machine or consider using a rootless Docker setup.
- Test the configuration by running the runner in the foreground:
forgejo-runner --config /etc/forgejo-runner/config.yamlYou should see log lines indicating the runner has registered and is polling for jobs, e.g.:
INFO[0000] Starting forgejo-runner version ... INFO[0001] Registered runner with ID 12 INFO[0002] Listening for jobs...- To run the runner as a background service, create a systemd unit file:
# /etc/systemd/system/forgejo-runner.service [Unit] Description=Forgejo Actions Runner After=network-online.target Wants=network-online.target [Service] Type=simple User=%i # replace with the runner user, e.g., forgejo ExecStart=/usr/local/bin/forgejo-runner --config /etc/forgejo-runner/config.yaml Restart=on-failure LimitNOFILE=65535 [Install] WantedBy=multi-user.targetEnable and start it:
sudo systemctl daemon-reload sudo systemctl enable --now forgejo-runner.service -
Add a workflow that uses the runner labels
In any repository, create the directory
.forgejo/workflows(if it does not exist) and add a YAML file, for exampleci.yml:# .forgejo/workflows/ci.yml name: CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: [self-hosted, docker, ubuntu-latest] # must match runner labels steps: - name: Checkout code uses: actions/checkout@v4 - name: Echo hello run: echo "Hello from Forgejo Actions running in Docker"Commit and push the file. In the Forgejo UI, open the repository, go to the Actions tab, and you should see a workflow run triggered. The job will appear as "pending" until the runner picks it up, then transition to "in progress" and finally "completed".
Expected checks
- Runner logs (via
journalctl -u forgejo-runner.serviceor stdout if run manually) show a successful registration and periodic "Listening for jobs" messages. - In the Forgejo UI under Site Administration → Actions → Runners, the runner appears as Idle or Busy when a job is running.
- The workflow run in the repository’s Actions tab moves from
queued→in progress→completedwithout errors. - Inspect the job’s logs in the UI; you should see the output of the
echostep and any Docker container startup messages.
Recovery options (rollback)
Because this procedure installs a runner binary, creates a configuration file, and optionally adds a systemd service, you can reverse the changes:
- Stop and disable the systemd service:
sudo systemctl stop forgejo-runner.service
sudo systemctl disable forgejo-runner.service
sudo rm /etc/systemd/system/forgejo-runner.service
sudo systemctl daemon-reload
- Remove the runner binary and configuration directory:
sudo rm /usr/local/bin/forgejo-runner
sudo rm -rf /etc/forgejo-runner
- If you added the user to the
dockergroup solely for this runner, you may remove them (optional):
sudo deluser $USER docker
newgrp # refresh group membership
After removal, any existing workflow runs that were depending on this runner will stay in a "pending" state until another matching runner becomes available or you cancel them.
Limitations and practical verification
- The runner’s Docker mode requires the runner process to access the host’s Docker socket (
/var/run/docker.sock). This grants the runner the ability to start containers with the same privileges as the Docker daemon. If you cannot trust the workflows, consider using a rootless Docker configuration or a VM‑based runner instead. - Label matching is strict: the list of labels in
runs-onmust be a superset of the runner’s labels. A mismatch leaves jobs stuck in "pending". Verify labels in both the runner’sconfig.yamland the workflow file. - Secrets defined in the repository settings are injected as environment variables at runtime and are not stored in the workflow file. Ensure that secret names match exactly what the workflow references.
- To confirm that secrets are not leaked, you can add a step that prints the environment and verify that no secret values appear in the job logs.
Note: This guide reflects the behavior of Forgejo Actions as of the 1.20 series. Adjust paths, version numbers, or service names if your installation differs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.