Five-issue post-mortem of wiring up the Android APK workflow on Forgejo 10 + act_runner. Closed on run #176 by switching ROOT_URL to forgejo.localhost. |
||
|---|---|---|
| docs | ||
| runner-image | ||
| runner-image-android | ||
| scripts | ||
| templates | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.caddy.yml.example | ||
| docker-compose.yml | ||
| README.md | ||
| setup.sh | ||
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 atssh://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_USERin.env(defaults toforgejo_admin— Forgejo reservesadminas a username) - Admin password: see
FORGEJO_ADMIN_PASSWORDin.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:
@usernamementions 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:
- Edit
.env:FORGEJO_DOMAIN=forge.example.com FORGEJO_SSH_DOMAIN=forge.example.com FORGEJO_ROOT_URL=https://forge.example.com/ - (Public TLS only) Copy the Caddy override and bring the stack up with it:
Caddy provisions Let's Encrypt automatically. Ports 80 and 443 must be reachable from the public internet.cp docker-compose.caddy.yml.example docker-compose.caddy.yml docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d - Re-run
./setup.shso the bootstrap reflects the new URLs in workflows and memory.
Security notes
- The
forgejo-runnercontainer 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 is984; override withDOCKER_GIDin.envif 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.envand as Forgejo Actions secrets. Keep.envout of git (already gitignored). bootstrap-forgejo.shdeletes 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 createrejects the username: Forgejo reserves certain usernames (admin,api,assets, …). Pick another value forFORGEJO_ADMIN_USERin.env. The default isforgejo_admin.- SSH on port 22 is wedged or "address already in use": do not enable
START_SSH_SERVER=truein the Forgejo container — the upstream image already bundles its own SSH stack and the two collide. The compose file ships withSTART_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
.envworks (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 withgetent group docker | cut -d: -f3and setDOCKER_GIDin.env, thendocker 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 checksdata/runner/.runnerinstead — that file is written byforgejo-runneron successful registration. docker execintoforgejo-mcpreturns "exec: "/bin/sh": not found": theronmi/forgejo-mcpimage isFROM scratch— there is no shell, nols, just the binary. The long-running container exposes an HTTP MCP endpoint (port8181by default); Claude Code itself launches a freshdocker run --rm -i … stdiocontainer per session via the entry in~/space/.mcp.json.- MCP not visible to Claude Code:
cat ~/space/.mcp.jsonshould showmcpServers.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 theskip-bot-reviewlabel. - 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/secretsreturns 422 [Data]: Required: Forgejo 10 rejects empty string secrets. The bootstrap detects this andDELETEs 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