Choosing CI for Self-Hosted Forgejo: Actions vs External Systems
Decision guide for choosing between Forgejo Actions and external CI (Woodpecker, Jenkins) on self-hosted Forgejo. Covers constraints, trade-offs, runner isolation, and a concrete validation path.
03 Dec 2025, 06:11 UTC

The Decision You're Facing
You run a self-hosted Forgejo instance and need continuous integration. The core choice is between adopting Forgejo Actions—the integrated, GitHub Actions-compatible workflow engine—or keeping an external CI system like Woodpecker CI, Jenkins, or Drone that connects via webhooks and commit-status APIs. This guide walks through the constraints, compares the supported options, and gives you a concrete validation path so you can decide with evidence rather than assumptions.
Constraints That Shape the Choice
| Constraint | Forgejo Actions | External CI (Woodpecker, Jenkins, Drone) |
|---|---|---|
| Workflow syntax | YAML in .forgejo/workflows or .gitea/workflows; mirrors GitHub Actions schema | Separate YAML dialect per system (Woodpecker .woodpecker.yml, Jenkinsfile, Drone .drone.yml) |
| Runner model | forgejo-runner processes register via token; Docker or host execution | System-specific agents/runners; often more mature pooling and labeling |
| Authentication & permissions | Integrated with Forgejo users, teams, orgs; per-job tokens | Separate auth (OAuth, LDAP, tokens); must sync with Forgejo |
| UI & log visibility | Runs, logs, status checks inside Forgejo repository Actions tab | External dashboard; status badges via commit-status API |
| Ecosystem compatibility | Subset of GitHub Actions; marketplace actions may fail on GitHub-specific APIs | Native plugin ecosystems; Woodpecker has Forgejo-native integration |
| Operational overhead | One service (Forgejo) + runner processes | Forgejo + separate CI server + agents |
| Scaling model | Add runners; limited matrix caching, no custom agent pools | Advanced pooling, labels, heterogeneous farms, priority queues |
| Security posture | Host-mode runners expose host; container mode + fork restrictions required | Varies; Woodpecker isolates by default, Jenkins needs hardening |
Trade-Off Analysis
When Forgejo Actions Fits
- Small to medium teams already using Forgejo for code review and issue tracking who want zero-context-switching CI.
- GitHub Actions migrants with workflows that use standard actions (
actions/checkout,actions/setup-node,docker/build-push-action) and avoid GitHub-specific services (GitHub Packages, Environments, OIDC federation to cloud providers via GitHub). - Simplified compliance: audit logs, permissions, and artifact retention stay in one application.
When External CI Wins
- Heterogeneous build farms: macOS, Windows, ARM, or GPU nodes with custom labels and priority scheduling.
- Complex caching/matrix needs: Woodpecker and Jenkins offer richer cache keys, cross-repo artifact sharing, and dynamic matrix generation.
- Existing investment: teams with maintained Jenkins pipelines, shared libraries, or Drone plugins that would be costly to rewrite.
- Multi-forge strategies: if you also build from GitLab, Bitbucket, or plain Git, a forge-agnostic CI avoids coupling CI to one forge.
Security: The Runner Isolation Decision
Forgejo Actions runners operate in two modes:
- Host mode: job steps run directly on the runner host. Simple and fast, but any compromised workflow gets host access—including the runner registration token and potentially the Forgejo data if co-located.
- Container mode: each job runs in a Docker container. The workflow YAML can still request privileged containers or host mounts, so you must enforce policy via runner labels and repository-level settings.
Standard hardening steps for self-hosted runners:
- Run runners on dedicated VMs or isolated hosts, not the Forgejo application server.
- Prefer the container executor; avoid host execution for any repository that accepts outside contributions.
- Restrict pull-request workflows from forks: in repository Settings → Actions, disable fork PR workflows or require approval for first-time contributors.
- Scope runner registration tokens to the narrowest level (repository, then organization, then instance) that your topology allows.
Concrete Validation Path
Before committing to either approach, run this end-to-end test on a staging Forgejo instance. It proves the integration works and surfaces version-specific quirks. Exact flag names, workflow directory names, and runner versions are version-sensitive—check the documentation for your installed Forgejo release first.
Prerequisites
- A Forgejo instance with admin access (check your release's docs for Actions support and defaults).
- A Linux host with Docker Engine installed for the runner.
- A test repository you can push to.
Step 1: Enable Actions on the Instance
In the Forgejo admin configuration, locate the Actions section and enable it (the exact key name and default vary by release; consult your version's config cheat sheet). Save and restart Forgejo if required. Verify the Actions tab appears on repositories.
Step 2: Register a Runner
On the runner host (not the Forgejo server), download the forgejo-runner release matching your Forgejo version, then register it at repository scope:
./forgejo-runner register \
--instance https://forgejo.example.com \
--token REPOSITORY_REGISTRATION_TOKEN \
--name test-runner-1 \
--labels "ubuntu-latest:docker://node:20-bookworm"
Where to run: the runner host, as a dedicated unprivileged user. Placeholders: forgejo.example.com is your instance URL; the registration token comes from the repository's Settings → Actions → Runners page (or org/admin settings for wider scopes). Expected check: after registration and starting the runner (./forgejo-runner daemon), the runner shows as online in the same settings page. Risks: the token grants runner registration rights—avoid shell history exposure by using an environment variable or interactive prompt; if the runner never comes online, workflows will sit queued indefinitely, so verify status before proceeding. Exact register flags differ between runner versions; run ./forgejo-runner register --help to confirm.
Step 3: Commit a Minimal Workflow
In the test repository, create .forgejo/workflows/ci.yml (use .gitea/workflows if your version's docs specify it):
name: validation
on:
push:
branches: [main]
jobs:
echo-sha:
runs-on: ubuntu-latest
steps:
- name: Print commit SHA
run: echo "Commit: ${GITHUB_SHA}"
Push to main. Open the repository Actions tab. You should see a run named "validation" with a green status, and the commit itself should show a success check. Click into the run to confirm the log prints the commit SHA. If the run stays queued, the runner label in runs-on doesn't match any registered runner's labels.
Step 4: Test Fork PR Restriction
- Fork the test repo as a different user.
- Create a branch, modify a file, open a pull request against upstream
main. - Confirm the workflow does not run automatically (or waits for approval, depending on your setting).
- Merge the PR; verify the workflow runs on the merge commit.
This validates the fork-protection control that prevents untrusted code execution on your runners—the single most important security check for a public-facing instance.
Limitations and Version Sensitivity
- Workflow directory names: recent Forgejo versions prefer
.forgejo/workflows; older versions and Gitea use.gitea/workflows. Check your instance's documentation. - Runner compatibility:
forgejo-runnerreleases track Forgejo releases; mismatched versions may fail to register or execute jobs. Pin both together. - Action availability: the
uses:keyword resolves actions from a configured mirror. Actions that call GitHub-specific APIs will fail. Test every third-party action you depend on before migrating. - Caching: cache behavior depends on runner-local storage or separately configured backends; do not assume cross-runner cache sharing works out of the box.
- Feature parity: Forgejo Actions deliberately mirrors much of GitHub Actions syntax, but parity is not guaranteed and differs between Forgejo and upstream Gitea.
Verification Checklist Before Production
- Confirm your Forgejo version and read its specific Actions documentation.
- Register a runner against a test repo; verify online status in the UI.
- Run a push-triggered workflow; confirm commit status and logs render in Forgejo end to end.
- Test a PR from a fork; verify it respects the fork-restriction setting.
- Port one real workflow from your current CI; compare run time, cache behavior, and any failed steps.
- Document the runner update procedure (binary replacement, config reload) for your ops runbook.
If validation passes and your workflows don't rely on GitHub-specific services, Forgejo Actions reduces operational surface area. If you hit matrix limits, need non-Linux runners, or have a mature Jenkins estate, the external CI path remains valid—just factor in the ongoing cost of maintaining two systems.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.