Skip to main content
Private preview. fremforge is in private preview — invited customers only. Content is still subject to change. Request access →
CI runners

CI runners

fremforge provides hosted runners included in every seat plan, and supports BYO runners for teams that need on-prem, custom hardware, or specific network placement. Both runner types execute Forgejo Actions workflows using the same GitHub Actions YAML syntax.

Hosted runners

Hosted runners are ephemeral, kernel-isolated compute provisioned on T Cloud Cloud Container Instance (CCI) in the eu-de region. Each job gets a fresh instance; nothing persists between runs.

Available labels

LabelOS / archNotes
fremforgeAlpine 3.22, x86_64Default hosted-runner label. Use this for nearly every workflow. See Runner image contents for what’s pre-installed.
linux/amd64Alpine 3.22, x86_64Synonym for fremforge. Targets the same image.
cciAlpine 3.22, x86_64Synonym for fremforge, surfaces the underlying T Cloud CCI runtime in the label name for workflows that want to be explicit about where they run.

Reference a label in your workflow:

jobs:
  build:
    runs-on: fremforge

For container image builds, use the pre-installed kaniko (Docker-in-Docker is not available, see Container builds below):

jobs:
  build-image:
    runs-on: fremforge
    steps:
      - uses: actions/checkout@v4
      - run: |
          /usr/local/bin/kaniko \
            --context=$GITHUB_WORKSPACE \
            --dockerfile=Dockerfile \
            --destination=registry.example.com/$GITHUB_REPOSITORY:$GITHUB_SHA

ARM64 hosted runners and additional OS variants (Debian, Fedora, …) are on the Phase 2 roadmap, fremforge today targets x86_64 Linux only. BYO runners (see below) are the path for ARM64 or non-Linux workflows in the meantime.

Isolation model

Each job runs inside a dedicated CCI instance with its own kernel. There is no shared kernel between jobs in the same org, different orgs, or between concurrent runs of the same workflow. The host hypervisor is T Cloud’s CCI fleet in eu-de (Biere/Magdeburg).

Included minutes and concurrency

DetailValue
Included minutes1,000 runner minutes/seat/month, pooled across the org
Concurrency (hard cap)2 concurrent jobs per seat, max 100/org
Per-job runtime ceiling6 hours (matches GitHub Actions max). Job-level timeout-minutes: in your workflow YAML caps shorter values; step-level timeout-minutes: already honored.
Per-minute charge within poolNone, included in the flat €30/seat plan
When pool is exhaustedNew jobs are refused with a clear fremforge/runner-minutes-cap commit-status check; in-flight jobs finish naturally. Overage billing is opt-in, see below.
Minutes reset1st of each month; unused minutes do not roll over

The concurrency limit is a soft limit. Contact support@frem.sh to raise it for your org. During platform-wide peak, a hard cap applies; jobs queue rather than fail.

Overage billing (opt-in): by default, new jobs queue when the pool is exhausted. Org owners can enable metered overage at Org admin → Billing → Runner minutes → Enable overage billing. When enabled, minutes above the pool are charged at €0,01/min on the next invoice. Overage can be disabled at any time from the same page.

Usage dashboard

Track runner minute consumption at Org admin → Billing → Runner usage.

The usage dashboard shows:

ViewWhat it shows
Monthly summaryTotal minutes used this billing period vs the included pool (1,000 min × seat count)
Per-repository breakdownMinutes consumed by each repository, sortable by usage
Per-workflow breakdownMinutes consumed by each workflow file within a repository
TimelineDaily usage chart for the current and previous billing periods

Minutes are counted from job start to job end, rounded up to the nearest second. Queue time (waiting for a runner) is not counted.

The monthly period resets on the 1st of each month UTC. Unused minutes do not roll over.

BYO runner minutes are tracked separately in the same dashboard and do not count against the hosted pool.

Alerts: set a usage alert at Org admin → Billing → Runner usage → Set alert. You receive an email when the org reaches the configured threshold (e.g., 80% of the monthly pool). Useful for orgs that have overage billing enabled and want advance warning before the pool is exhausted.

Runner image contents

The hosted runner image is rebuilt weekly. A Trivy CVE gate runs at build time. Any image with HIGH or CRITICAL findings is blocked from shipping until patched.

We deliberately keep the image lean (~280 MB compressed; GitHub’s ubuntu-latest is ~50 GB) by NOT pre-installing language toolchains. Customers pin specific language versions via standard setup-* actions, same UX as GitHub Actions, with the benefit of explicit per-workflow version pinning rather than implicit drift on the host image.

Pre-installed:

GroupTools
OS baseAlpine 3.22
Shell + GNU coreutilsbash, coreutils, util-linux, findutils, grep, sed, gawk, diffutils
Network + cryptoca-certificates, curl, wget, openssl, openssh-client
VCSgit, git-lfs (initialised system-wide)
Archivestar, gzip, xz, bzip2, unzip, zip
Build essentialsbuild-base (gcc 14.x + make), pkgconf, autoconf, automake, libtool, cmake, linux-headers
Data shapejq, yq
JS runtimeNode 22 LTS + npm
Lint helpersshellcheck, shfmt
Security scangitleaks v8.30.1, trivy 0.70.0
Container buildkaniko v1.23.2 (rootless OCI image build, Docker daemon NOT available, see below)

Use setup-* actions for: Python, Go, Rust, Java, .NET, Ruby, PHP, kubectl, helm, cloud CLIs (T Cloud OTC CLI, AWS CLI, Azure CLI, gcloud).

The full manifest with exact resolved versions + SWR digest + Trivy scan summary + CycloneDX SBOM is posted to the fremforge changelog with each weekly image rebuild.

Container builds (no Docker-in-Docker)

Our runner pods run on T Cloud Cloud Container Instance (CCI) v2, which doesn’t permit privileged containers, Docker-in-Docker is therefore not possible. Use the pre-installed kaniko instead for rootless OCI image builds:

- name: Build + push image
  run: |
    /usr/local/bin/kaniko \
      --context=$GITHUB_WORKSPACE \
      --dockerfile=Dockerfile \
      --destination=registry.example.com/$GITHUB_REPOSITORY:$GITHUB_SHA

For pure image copy (e.g. retag-and-push between registries), apk add skopeo works in a run: step.

For full per-tool details see docs.frem.sh/build/actions/runner-image/.

BYO runners

Register your own runners against any fremforge org (on-prem servers, cloud VMs, Mac minis, Raspberry Pis). BYO runner minutes do not count against the hosted pool.

Registration

  1. Go to Org admin → Settings → Runners → New runner.
  2. Copy the registration token.
  3. Download the Forgejo runner binary. The current stable release is published at code.forgejo.org/forgejo/runner/releases, pin to a recent tag when you install:
# Linux x86_64 — replace <version> with the tag from the releases page (e.g. v6.4.0)
curl -sSL https://code.forgejo.org/forgejo/runner/releases/download/<version>/forgejo-runner-<version>-linux-amd64 \
  -o forgejo-runner && chmod +x forgejo-runner

# macOS arm64
curl -sSL https://code.forgejo.org/forgejo/runner/releases/download/<version>/forgejo-runner-<version>-darwin-arm64 \
  -o forgejo-runner && chmod +x forgejo-runner

fremforge tracks current Forgejo releases; runners on older binaries continue to work but should be upgraded within a release cycle to pick up CVE patches.

  1. Register the runner:
./forgejo-runner register \
  --instance https://frem.sh \
  --token <registration-token> \
  --name my-runner \
  --labels linux,self-hosted,my-custom-label
  1. Start the runner:
./forgejo-runner daemon

Supported platforms

PlatformArchitectures
Linuxx86_64, ARM64, ARMv7
macOSx86_64, Apple Silicon (ARM64)
Windowsx86_64

Custom labels

Labels assigned at registration are what you reference in runs-on::

jobs:
  deploy:
    runs-on: [self-hosted, my-custom-label]

Multiple labels in a list mean “find a runner that has ALL of these labels”. The self-hosted label is conventional. Include it to avoid accidentally routing a BYO job to a hosted runner.

Don’t use reserved labels. The labels fremforge, fremforge-cci, cci, and linux/amd64 route to our hosted-runner pool. Pick distinct labels for your BYO runners (e.g. self-hosted, on-prem, my-team-runner) so workflows can address them unambiguously.

Network requirements for the runner host

The runner binary opens long-poll HTTPS connections to https://frem.sh and clones repositories over HTTPS. From the host’s perspective the connectivity requirement is:

DestinationPortPurpose
frem.sh443Connect-RPC runner protocol (/api/actions/runner.v1.RunnerService/*), Actions runtime APIs, git clone over HTTPS
*.frem.sh443Reserved for tenant subdomains, attachment + raw-file downloads
Action mirror (transitive)443When a workflow uses uses: actions/<name>@<ref>, the runner fetches the action from frem.sh/mirrors/... — same host as above
Customer workflow targetsvariesWhatever destinations your workflows hit (deploy targets, package registries, etc)

No inbound connectivity is needed — the runner initiates all connections outbound. A runner behind NAT or a firewall works fine as long as it can reach frem.sh:443.

Security and isolation responsibility

For hosted runners, fremforge provides kernel-level isolation between jobs (each job runs in a fresh CCI micro-VM). For BYO runners, isolation is your responsibility. A self-hosted runner that handles workflows from multiple repositories shares state across jobs unless you scope each runner to a single repository or org.

If you run workflows from multiple teams or trust-domains, treat your BYO runner the same way GitHub Actions self-hosted runners are treated:

  • One runner per repository (or per trust-boundary) when secrets are sensitive.
  • Don’t register a BYO runner against a public repository — anyone with merge access could land a workflow change that executes arbitrary code on your runner host.
  • Treat the runner host as production: harden the OS, keep the runner binary patched, and route its egress through your own SSRF / egress controls.

The fremforge SSRF outbound proxy does NOT apply to BYO runners — only to hosted runners. Your runner’s network policy is whatever you wire on the host.

Egress and network

RuleDetail
Outbound HTTP/HTTPSAllowed (ports 80 and 443)
Outbound other portsBlocked by default
Inbound connectionsBlocked, runners do not accept inbound connections
T Cloud metadata endpoint (169.254.x.x)Blocked by SSRF outbound proxy
RFC-1918 private rangesBlocked by SSRF outbound proxy

Org-level egress allowlist: to allow outbound access to additional CIDRs or ports from runner jobs, contact support@frem.sh. Egress rules are configured at the platform level.

The SSRF proxy is a platform floor. It cannot be disabled. It prevents runner jobs from reaching T Cloud instance metadata or internal infrastructure.

Secrets in jobs

Secrets are injected as environment variables and masked in job logs. Any log line containing a secret value is replaced with ***.

Reference syntax:

steps:
  - name: Deploy
    env:
      API_KEY: ${{ secrets.MY_API_KEY }}
    run: ./deploy.sh

Secret scopes

ScopeWhere to setAvailable to
Org secretsOrg admin → Settings → SecretsAll repositories in the org
Repo secretsRepository → Settings → SecretsThat repository only
Environment secretsRepository → Settings → Environments → <env> → SecretsJobs that reference that named environment

Environment secrets require the job to declare an environment:

jobs:
  deploy:
    runs-on: fremforge
    environment: production
    steps:
      - run: echo ${{ secrets.PROD_API_KEY }}

For cloud provider credentials, prefer OIDC token federation over long-lived secrets. No secret to store, no secret to rotate.

Troubleshooting

Job is stuck in queue.

The most common cause is hitting the concurrent-job cap (2/seat, max 100/org). Check Org admin → Billing → Runner usage for current active job count. If you are at the cap, either wait for a running job to finish, add more seats (which raises the cap proportionally), or contact support@frem.sh for an Enterprise-on-Demand uplift.

docker command not found in job.

Hosted fremforge runners do NOT include the Docker daemon or the docker CLI, CCI v2 (the underlying T Cloud runtime) doesn’t permit privileged containers, so Docker-in-Docker is not possible. For container image builds, use the pre-installed kaniko (see Container builds). For pure image copy/retag/push between registries, apk add skopeo works in a run: step.

Secret is masked in logs but the step behaves as if it is empty.

The secret exists in the runtime (masking confirms this), but the value at the correct scope is not set. Check:

  1. Is the secret set at org level, repo level, or environment level?
  2. If it is an environment secret, does the job declare environment: <name> matching the secret’s environment?
  3. Is the secret name spelled exactly right (case-sensitive)?

The Secrets UI at the relevant scope shows the secret name and last-updated timestamp. Use that to confirm the secret exists before looking at the workflow.

Cross-references