Forgejo + Forgejo Actions on Proxmox: Self-Hosting Your Own Code Forge with Real CI in 2026
Self-Hosting

Forgejo + Forgejo Actions on Proxmox: Self-Hosting Your Own Code Forge with Real CI in 2026

Ricardo Gil
May 4, 2026
8 min read
#Forgejo #Forgejo Actions #Self-Hosting #Proxmox #CI/CD #Home Lab #Git #LXC

I migrated my personal repos off GitHub last weekend. Not because of any drama β€” GitHub is fine β€” but because I wanted full control of my CI runners, my secrets, and my build artifacts on the same hardware that runs the rest of my home lab. Forgejo was the obvious landing spot, and Forgejo Actions has matured to the point where the migration is mostly painless: copy your .github/workflows directory to .forgejo/workflows, wire up a runner, and you're done.

This post walks through what I actually built: Forgejo running in a Proxmox LXC with Forgejo Actions enabled, a separate runner LXC with Docker, and a real-world CI pipeline doing builds and pushes. The whole thing runs on a Beelink SER8 with the Ryzen 7 8845HS sitting on my rack, and it's been rock solid for three weeks.

Why Forgejo over Gitea (in 2026)

A quick reality check before you spend an afternoon installing software you'll regret. Gitea and Forgejo are still nearly identical at the codebase level β€” Forgejo started as a hard fork in late 2022 over governance concerns when Gitea Ltd. was spun out as a for-profit company. Three years later, they've diverged enough that the choice actually matters.

Forgejo's selling points in 2026: nonprofit governance under Codeberg e.V., GPL licensing (Gitea is MIT), monthly security backports, and Forgejo Actions getting upstream features before Gitea Actions does. Gitea's pitch is the enterprise tier β€” cloud sync, package proxy, organizational features β€” which is irrelevant if you're self-hosting for yourself or a small team. For a home lab, Forgejo wins on principle and on cadence. I picked it.

If you're already running Gitea, don't migrate for the sake of it. The export/import path works but it's not seamless, and the day-to-day experience is identical. Pick Forgejo for greenfield installs.

The architecture

Two LXCs on Proxmox, talking over the LXC bridge:

  • forgejo-lxc β€” Debian 12 unprivileged container, 2 vCPU, 4 GB RAM, 32 GB disk. Runs Forgejo + PostgreSQL.
  • forgejo-runner-lxc β€” Debian 12 privileged container (nesting=1, keyctl=1), 4 vCPU, 8 GB RAM, 64 GB disk. Runs the Forgejo Actions runner and Docker.
  • Why split the runner into its own LXC? Two reasons. First, the runner needs to execute arbitrary code from your repos β€” keep that blast radius isolated from the database. Second, Docker-in-LXC requires a privileged container with nesting enabled, and you don't want your git server to need those permissions.

    If you're running this on a dedicated mini PC instead of a beefy Proxmox host, an N100 mini PC like the Beelink Mini S12 handles the Forgejo container fine β€” but you'll want something with more cores like the GMKtec NucBox K8 or the SER8 mentioned above for the runner.

    Forgejo install

    I'm running Forgejo as a binary under systemd, not Docker. The binary path is simpler for backups and updates, and the resource overhead difference vs. Docker is real on small LXCs.

    bash
    # In forgejo-lxc, as root
    apt update && apt install -y postgresql-15 git curl
    useradd --system --create-home --home-dir /var/lib/forgejo \
      --shell /bin/bash --comment "Forgejo" git

    # Create the database sudo -u postgres psql -c "CREATE USER forgejo WITH PASSWORD 'changeme';" sudo -u postgres psql -c "CREATE DATABASE forgejo OWNER forgejo;"

    # Download the latest binary (check forgejo.org/releases for current) FORGEJO_VERSION=10.0.1 curl -fsSL -o /usr/local/bin/forgejo \ https://codeberg.org/forgejo/forgejo/releases/download/v${FORGEJO_VERSION}/forgejo-${FORGEJO_VERSION}-linux-amd64 chmod +x /usr/local/bin/forgejo

    # Directory layout mkdir -p /var/lib/forgejo/{custom,data,log} chown -R git:git /var/lib/forgejo chmod -R 750 /var/lib/forgejo mkdir -p /etc/forgejo && chown root:git /etc/forgejo && chmod 770 /etc/forgejo

    Drop a systemd unit at /etc/systemd/system/forgejo.service β€” Forgejo's docs have a clean reference one β€” then systemctl enable --now forgejo. Hit http://forgejo-lxc:3000, run the installer, and create your admin account.

    Two settings to flip in /etc/forgejo/app.ini after install:

    ini
    [server]
    ROOT_URL = https://git.yourdomain.local/
    DOMAIN = git.yourdomain.local

    [actions] ENABLED = true DEFAULT_ACTIONS_URL = github

    DEFAULT_ACTIONS_URL = github is what makes most existing GitHub Actions work without modification β€” when a workflow references uses: actions/checkout@v4, Forgejo will fetch it from github.com instead of expecting a local mirror. Restart Forgejo. You now have a self-hosted code forge with CI capability.

    The Forgejo Actions runner

    This is where the interesting work happens. The runner is a separate Go binary that polls Forgejo for jobs and executes them β€” by default in Docker containers, which is exactly what you want.

    Inside forgejo-runner-lxc (after enabling nesting in the LXC config and installing Docker):

    bash
    RUNNER_VERSION=6.2.2
    curl -fsSL -o /usr/local/bin/forgejo-runner \
      https://code.forgejo.org/forgejo/runner/releases/download/v${RUNNER_VERSION}/forgejo-runner-${RUNNER_VERSION}-linux-amd64
    chmod +x /usr/local/bin/forgejo-runner

    useradd --system --create-home --home-dir /var/lib/forgejo-runner \ --shell /bin/bash forgejo-runner usermod -aG docker forgejo-runner

    # Generate the runner config sudo -u forgejo-runner forgejo-runner generate-config > \ /var/lib/forgejo-runner/config.yml

    Edit /var/lib/forgejo-runner/config.yml and change a few defaults:

    yaml
    runner:
      capacity: 2  # how many jobs to run concurrently
      timeout: 30m
      labels:
        - "ubuntu-latest:docker://node:20-bookworm"
        - "ubuntu-22.04:docker://node:20-bookworm"
        - "self-hosted:host"

    cache: enabled: true dir: "/var/lib/forgejo-runner/cache"

    container: network: "bridge" privileged: false options: ""

    Get a registration token from Forgejo's admin panel under Site Administration β†’ Actions β†’ Runners β†’ "Create new Runner", then register:

    bash
    sudo -u forgejo-runner forgejo-runner register \
      --no-interactive \
      --instance http://forgejo-lxc:3000 \
      --token <your-token> \
      --name "homelab-runner-1" \
      --labels "ubuntu-latest,ubuntu-22.04,self-hosted"
    

    Wrap it in a systemd unit and start it. You should see the runner appear as "online" in the Forgejo admin panel within ~10 seconds.

    A real workflow

    Here's a minimal .forgejo/workflows/ci.yml from one of my Node projects, the kind of thing that actually exercises the runner:

    yaml
    name: CI
    on:
      push:
        branches: [main]
      pull_request:

    jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - run: npm ci - run: npm run lint - run: npm test

    build-and-push: needs: test if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Login to Forgejo registry run: echo "${{ secrets.REGISTRY_TOKEN }}" | \ docker login forgejo-lxc:3000 -u ${{ github.actor }} --password-stdin - run: docker build -t forgejo-lxc:3000/ricardo/myapp:${{ github.sha }} . - run: docker push forgejo-lxc:3000/ricardo/myapp:${{ github.sha }}

    The Forgejo container registry is enabled by default and lives at the same hostname as the web UI β€” no separate Harbor or Nexus needed. For most home lab use cases this is enough; the registry handles OCI images, npm, Maven, and a dozen other formats out of the box.

    The gotchas nobody mentions

    Cache misses are common. Forgejo's actions cache is local to each runner. If you have multiple runners, jobs scheduled on different runners won't share cache. For a single-runner setup this doesn't matter; if you scale out, look at the cache_proxy setting and a shared S3-compatible backend like MinIO running on a Synology DS224+.

    Docker-in-Docker is rough in LXC. The runner spins up containers for each job, which works fine until a job tries to itself spin up a container (testcontainers, kind, etc.). The fix is nesting=1 on the LXC and using --privileged containers in your jobs, but at that point you're better off running the runner in a full VM. I have a separate Ubuntu Server VM for jobs that need real DinD.

    Secrets are per-repo or per-organization. Unlike GitHub, there's no environment scoping out of the box (it's coming, not here yet in 10.x). Plan your secret strategy accordingly β€” I keep deploy keys per-repo and infra credentials at the org level.

    Don't expose the runner. The Forgejo web UI is fine to expose via a Cloudflare Tunnel or reverse proxy. The runner should never be reachable from outside your LAN β€” it doesn't need to be. The runner polls out to Forgejo, not the other way around.

    Backups matter. Forgejo is one forgejo dump command away from a clean tarball, but the runner's cache and Docker volumes are not in that dump. I run Restic against a Backblaze B2 bucket for the Forgejo data dir nightly, and I don't bother backing up the runner β€” it's recreatable from config.

    Hardware notes

    You don't need much. Forgejo idles at ~150 MB RAM and the runner only uses memory while jobs are active. The bottleneck for CI throughput is almost always I/O β€” git clones, npm installs, Docker layer caching β€” so prioritize a fast NVMe over more cores.

    A single WD Black SN850X 2TB on a Proxmox host gives you headroom for Forgejo, a dozen other LXCs, and CI cache without thrashing. If you're building a dedicated git/CI box, the Beelink SER8 or Minisforum UM790 Pro hits the sweet spot β€” 8 cores, 32 GB DDR5, room for two NVMe drives.

    For backups, a Synology DS923+ on the same VLAN gives you a Restic target without a cloud bill, though I still run B2 as a second tier for offsite. RAM matters less than you'd think β€” even my 4 GB Forgejo LXC has never come close to swapping.

    Migration path from GitHub

    For each repo I wanted to move:

    1. Create a matching empty repo in Forgejo 2. git remote set-url origin http://forgejo-lxc:3000/ricardo/myrepo.git && git push --mirror 3. Copy .github/workflows/.yml to .forgejo/workflows/.yml (literal directory rename, no syntax changes for ~95% of workflows) 4. Replace any actions/cache@v4 references with https://code.forgejo.org/actions/cache@v4 if you want first-party caching, otherwise the GitHub fallback works 5. Set up secrets in the new repo's settings 6. Push, watch the runner pick it up

    The whole thing took me about 15 minutes per repo for ten repos. The longest part was reconfiguring Renovate to point at the new Forgejo URL.

    Is this worth doing?

    If you already have a working home lab and you push code regularly, yes. The clincher for me was build minutes β€” I was burning through the GitHub free tier on container builds for projects nobody but me cares about. Running CI on hardware I already own is essentially free, and the iteration speed on a local runner is noticeably faster than GitHub's hosted runners (no cold-start, no queue time, persistent layer cache).

    If you're a casual git user with three repos, just stay on GitHub. Forgejo's worth the operational overhead only when you're hitting a real GitHub limitation β€” billing, privacy, network egress, or compliance.

    For me, on a home lab that already has Proxmox, Tailscale, and a working backup story, this was the obvious next move. Three weeks in, I haven't touched it except to update the Forgejo binary once. That's the bar I want my self-hosted stack to clear.

    ---

    Disclosure: This post contains affiliate links. If you purchase through these links, I may earn a small commission at no extra cost to you.

    πŸ“¬Weekly Newsletter

    Get the best home lab & AI content

    No spam. One email per week. Unsubscribe anytime.

    Share this article