forgejo-stack/README.md

12 KiB

forgejo-stack

A self-hosted Forgejo + Claude Code integration in a portable Docker Compose stack. Tar it up, move it to any Linux box with Docker, untar, run ./setup.sh — done.

What's in the box

  • Forgejo at http://localhost:3000, SSH at ssh://git@localhost:2222
  • Postgres 16 as Forgejo's database
  • forgejo-runner for Forgejo Actions (GitHub Actions compatible)
  • forgejo-mcp sidecar so Claude Code can drive PR/issue/repo operations natively via MCP
  • Claude + Gemini review bots that auto-comment on PRs in any repo you opt in via bootstrap-repo.sh
  • GitHub migration via scripts/migrate-from-github.sh

Requirements

  • Linux (or WSL2)
  • Docker 24+ with the compose plugin
  • curl, jq, awk, git, ssh (usually preinstalled)
  • ~2 GB free disk (data grows with your repos)

Quick start

git clone <stack-repo-url> forgejo-stack
cd forgejo-stack
# Optional: edit .env.example to set your hostname/ports/API keys before
# first run. .env is auto-created from .env.example with random secrets
# filled in.
./setup.sh

The script is idempotent — re-running it is safe and a no-op for things that already exist.

When done:

  • Web UI: http://localhost:3000
  • Admin user: see FORGEJO_ADMIN_USER in .env (defaults to forgejo_admin — Forgejo reserves admin as a username)
  • Admin password: see FORGEJO_ADMIN_PASSWORD in .env

Run ./scripts/smoke-test.sh to verify the install end-to-end.

Configuration

Everything lives in .env. Notable knobs:

Var Default Purpose
FORGEJO_DOMAIN localhost Used in every URL Forgejo emits
FORGEJO_SSH_DOMAIN localhost Used for SSH clone URLs
FORGEJO_HTTP_PORT 3000 Host port for the web UI
FORGEJO_SSH_PORT 2222 Host port for git-over-SSH
FORGEJO_ADMIN_USER forgejo_admin Admin username (cannot be admin — Forgejo reserves it)
ANTHROPIC_API_KEY (empty) Required for Claude review bot. If empty, bot skips.
GEMINI_API_KEY (empty) Required for Gemini review bot.
CLAUDE_BOT_ENABLED true Set false to disable Claude reviews globally
GEMINI_BOT_ENABLED true Set false to disable Gemini reviews globally
GITHUB_TOKEN (empty) Required for migrate-from-github.sh. Scopes: repo, read:org
DOCKER_GID 984 Host docker group GID — runner needs it to talk to the docker socket

After editing .env, re-run ./setup.sh to apply (it's idempotent).

Bot reviews

Both review bots run as Forgejo Actions workflows. Forgejo 10 does not propagate workflows from a <owner>/.forgejo repo to other repos under the same owner (verified empirically), so each repo has to opt in explicitly:

./scripts/bootstrap-repo.sh <owner>/<repo>
# or, with the default admin owner:
./scripts/bootstrap-repo.sh <repo>

That copies .forgejo/workflows/{claude,gemini}-review.yml into the target repo. The org-level repo <owner>/.forgejo (created by setup.sh) is the canonical source — bootstrap-repo.sh reads from it and falls back to the templates/ directory in this stack if the org repo file is missing.

After opt-in, both bots auto-review on PR open and on every push. To skip a single PR, add the skip-bot-review label.

To disable a bot for all repos: edit .env, set CLAUDE_BOT_ENABLED=false (or GEMINI_BOT_ENABLED=false), re-run ./setup.sh.

To customize the prompt for a single repo, commit .forgejo/prompts/claude-review.md (or gemini-review.md) into the repo — the workflow uses repo-local prompts when present and falls back to the defaults from the <owner>/.forgejo repo.

Migrating from GitHub

# Single repo (default forgejo owner = FORGEJO_ADMIN_USER)
./scripts/migrate-from-github.sh octocat/Hello-World

# All public repos owned by an account
./scripts/migrate-from-github.sh --all <gh-username>

# Include private repos (token must have repo scope)
./scripts/migrate-from-github.sh --all <gh-username> --include-private

# Replace an already-migrated target
./scripts/migrate-from-github.sh octocat/Hello-World --force

Known limitations of GitHub→Forgejo migration:

  • @username mentions in issues/PRs become orphaned (Forgejo can't resolve them to local users)
  • PR review threads can flatten into top-level comments
  • Reactions on comments may drop
  • GitHub Actions workflow files migrate as-is. Most work under Forgejo Actions; some actions/... references may need adjustment

After migration, run ./scripts/bootstrap-repo.sh <owner>/<repo> if you want bot reviews on the migrated repo.

Moving the stack to a new machine

# On the source box
docker compose down
tar czf forgejo-stack.tgz -C "$(dirname "$PWD")" "$(basename "$PWD")"

# Transfer however you like
scp forgejo-stack.tgz target:~/

# On the target
tar xzf forgejo-stack.tgz
cd forgejo-stack
./setup.sh --restore

--restore keeps existing Forgejo data (admin user, tokens, runner registration, repos all survive), and only re-creates the per-host artifacts that don't travel inside the data dirs:

  • ~/space/.mcp.json — registers the forgejo MCP server with Claude Code
  • ~/.claude/projects/-home-luffy-space/memory/forgejo-local.md — reference memory entry pointing Claude Code at the stack

Upgrading to LAN or public hostname

The stack ships localhost-first. To expose on your LAN or the internet:

  1. Edit .env:
    FORGEJO_DOMAIN=forge.example.com
    FORGEJO_SSH_DOMAIN=forge.example.com
    FORGEJO_ROOT_URL=https://forge.example.com/
    
  2. (Public TLS only) Copy the Caddy override and bring the stack up with it:
    cp docker-compose.caddy.yml.example docker-compose.caddy.yml
    docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
    
    Caddy provisions Let's Encrypt automatically. Ports 80 and 443 must be reachable from the public internet.
  3. Re-run ./setup.sh so the bootstrap reflects the new URLs in workflows and memory.

Security notes

  • The forgejo-runner container mounts /var/run/docker.sock. That is effectively root on the host. If this matters to you, switch the runner to host-mode execution at the cost of slower job startup.
  • The runner container joins the host's docker group via group_add. The default GID is 984; override with DOCKER_GID in .env if your host uses a different group.
  • Bot tokens (CLAUDE_BOT_TOKEN, GEMINI_BOT_TOKEN) are stored as Forgejo Actions secrets. They are not visible in finished workflow logs by default, but anyone with admin access to your Forgejo can read them.
  • API keys (ANTHROPIC_API_KEY, GEMINI_API_KEY) live both in .env and as Forgejo Actions secrets. Keep .env out of git (already gitignored).
  • bootstrap-forgejo.sh deletes Forgejo Actions secrets when their value is empty in .env (Forgejo rejects empty-string PUTs with 422). So leaving an API key blank disables the corresponding bot — it does not leave a stale secret behind.

Troubleshooting

  • Setup fails on wait_for_url: Forgejo is slow to start the first time on a fresh box. Re-run ./setup.sh; the container will already be up by then.
  • forgejo admin user create rejects the username: Forgejo reserves certain usernames (admin, api, assets, …). Pick another value for FORGEJO_ADMIN_USER in .env. The default is forgejo_admin.
  • SSH on port 22 is wedged or "address already in use": do not enable START_SSH_SERVER=true in the Forgejo container — the upstream image already bundles its own SSH stack and the two collide. The compose file ships with START_SSH_SERVER=false; leave it that way.
  • Runner stuck "waiting for /data/.runner-token": bootstrap writes the token then restarts the runner. If the file never appears, check that the admin token in .env works (curl -H "Authorization: token $FORGEJO_ADMIN_TOKEN" http://localhost:3000/api/v1/version).
  • Runner exits with permission denied … /var/run/docker.sock: the in-container UID isn't in the host's docker group. Find the GID with getent group docker | cut -d: -f3 and set DOCKER_GID in .env, then docker compose up -d --force-recreate runner.
  • No runner-list endpoint on Forgejo 10: Forgejo's API exposes only /admin/runners/registration-token, not a list endpoint. The smoke test checks data/runner/.runner instead — that file is written by forgejo-runner on successful registration.
  • docker exec into forgejo-mcp returns "exec: "/bin/sh": not found": the ronmi/forgejo-mcp image is FROM scratch — there is no shell, no ls, just the binary. The long-running container exposes an HTTP MCP endpoint (port 8181 by default); Claude Code itself launches a fresh docker run --rm -i … stdio container per session via the entry in ~/space/.mcp.json.
  • MCP not visible to Claude Code: cat ~/space/.mcp.json should show mcpServers.forgejo. Restart Claude Code so it re-reads the config.
  • Bot review didn't fire: the most common cause is forgetting to opt the repo in. Run ./scripts/bootstrap-repo.sh <owner>/<repo> once. Then check that the org-level secrets are set (Site Administration → Actions → Secrets), the runner is online, and the PR doesn't have the skip-bot-review label.
  • Bot review still skips after I added an API key: re-run ./setup.sh. API keys are pushed into Forgejo Actions secrets only when bootstrap runs. Without re-running, the workflow expands ${{ secrets.X }} to an empty string and short-circuits.
  • PUT /user/actions/secrets returns 422 [Data]: Required: Forgejo 10 rejects empty string secrets. The bootstrap detects this and DELETEs the secret instead, which lets workflows short-circuit cleanly. If you see this 422 directly, it's because something else is calling the API with an empty value.

Repository layout

forgejo-stack/
├── docker-compose.yml                  # 4-service stack: db, forgejo, runner, forgejo-mcp
├── docker-compose.caddy.yml.example    # opt-in TLS reverse proxy override
├── .env.example                        # all stack config
├── setup.sh                            # entry point — idempotent
├── scripts/
│   ├── lib.sh                          # shared bash helpers
│   ├── bootstrap-forgejo.sh            # provisions admin/tokens/bots/runner/secrets
│   ├── bootstrap-repo.sh               # opts a single repo into bot reviews
│   ├── migrate-from-github.sh          # GitHub → Forgejo migration
│   └── smoke-test.sh                   # end-to-end verification
├── templates/
│   ├── workflows/                      # claude-review.yml, gemini-review.yml
│   └── prompts/                        # default review prompts (markdown)
└── data/                               # all persistent state — gitignored
    ├── db/                             # postgres
    ├── forgejo/                        # forgejo (repos, users, lfs, …)
    └── runner/                         # forgejo-runner state