forgejo-self-host
Skill1 starsChanged 3 months agoMITView on GitHub
Use when the user wants a self-hosted, private Git server (repos, issues, pull requests, CI, wiki) running locally via Docker instead of relying on GitHub or GitLab.
- Reads credentials
- Commits and pushes
In this repository58 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- api-test-suiteSkill
- ascii-artSkill
- caddy-reverse-proxySkill
- changelog-generatorSkill
- color-palette-generatorSkill
- cron-taskSkill
- csv-toolkitSkill
- discord-bot-buildSkill
- docker-umbrellaSkill
- dotfiles-manageSkill
- email-sendSkill
- env-config-managerSkill
- excalidraw-diagramSkill
- file-organizerSkill
- frontend-design-toolkitSkill
- generate-dockerfileSkill
- gif-searchSkill
- git-backupSkill
- github-actions-ciSkill
- hallmark-readmeSkill
- http-api-testerSkill
- invoice-generatorSkill
- json-formatterSkill
- log-analyzerSkill
- markdown-linterSkill
- markdown-to-pdfSkill
- markdown-to-slidesSkill
- mcp-server-buildSkill
- media-id-mappingSkill
- movie-catalogue-siteSkill
- movie-night-calendarSkill
- ntfy-notifierSkill
- ocr-documentsSkill
- ollama-localSkill
- openapi-generatorSkill
- password-generatorSkill
- pdf-extractSkill
- portfolio-upkeepSkill
- qr-code-generatorSkill
- regex-testerSkill
- resume-builderSkill
- rss-monitorSkill
- searxng-self-hostSkill
- skill-publishSkill
- skill-registry-catalogSkill
- skills-portfolio-scaffoldSkill
- snippet-managerSkill
- sqlite-dashboardSkill
- streaming-provider-embedsSkill
- tailscale-deploySkill
- telegram-bot-buildSkill
- tmdb-metadataSkill
- uptime-kuma-self-hostSkill
- watchlist-syncSkill
- webhook-receiverSkill
- youtube-transcriptSkill
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 1 is distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 36
Docker Engine + Compose v2: Check with docker --version and docker compose version (the space-separated v2 form, not docker-compose).
- Very specific: line 46
If either is taken, change the left side of the port mapping and change ROOT_URL / SSH_PORT to match — see Pitfall 4.
- Very specific: line 56
Create a directory and a docker-compose.yml:
- Very specific: line 95
Do not add a version: key — Compose v2 warns the attribute 'version' is obsolete, it will be ignored.
- Very specific: line 258
Add your public key first under Settings → SSH / GPG Keys, or the push fails with Permission denied (publickey).
- Very specific: line 275
Use Forgejo's own dump command.
- Distinctive: line 284
If you must snapshot the raw volume instead, stop the container first, and note the Windows path caveat in Pitfall 9:
- Very specific: line 349
Port 3000 collides constantly: If you remap to 3001:3000, change ROOT_URL to http://localhost:3001/ in the same edit — remapping the port …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
forgejo-self-host
Overview
Deploy a self-hosted Forgejo instance using Docker. Forgejo is a lightweight, self-hosted Git server (a soft fork of Gitea) with repos, issues, pull requests, Actions CI, and a wiki.
The whole skill turns on one decision: configure the instance from environment variables and skip the web install wizard, rather than booting a blank container and clicking through it. A container booted without that configuration comes up in install mode, where the entire HTTP API is unreachable and the admin CLI refuses to run — which is the failure most first-time setups hit.
When to Use
- The user wants their own private Git server instead of GitHub/GitLab.
- The user wants to host repos locally for development or backup.
- The user wants CI/CD without relying on external services.
- The user says "set up Forgejo", "self-host my git", or "I want a local GitHub".
Prerequisites
- Docker Engine + Compose v2. Check with
docker --versionanddocker compose version(the space-separated v2 form, notdocker-compose). - On Windows: Docker Desktop with the WSL2 backend. The Linux-host tricks in
most Forgejo guides (
/etc/timezonebind mounts,$(pwd)indocker run) do not work there — see Pitfalls 2 and 9. - Two free host ports — one for HTTP, one for SSH. Defaults below are
3000and2222. Port3000collides with a lot of things (Grafana, Next.js, Rails, many dev servers), so check first:- Linux/macOS:
ss -ltnp | grep -E ':(3000|2222)\b' - Windows:
netstat -an | findstr /R /C:":3000 " /C:":2222 "If either is taken, change the left side of the port mapping and changeROOT_URL/SSH_PORTto match — see Pitfall 4.
- Linux/macOS:
- A named Docker volume, not a host bind mount, for
/data. Forgejo runs as uid 1000 inside the container; a Windows or macOS bind mount will not carry that ownership and the container fails to write its repositories.
Workflow
Step 1: Create the Forgejo deployment
Create a directory and a docker-compose.yml:
name: forgejo
services:
forgejo:
# Forgejo publishes NO `latest` tag — pin a major line.
image: codeberg.org/forgejo/forgejo:15
container_name: forgejo
restart: unless-stopped
environment:
USER_UID: "1000"
USER_GID: "1000"
TZ: "UTC"
# Skip the web install wizard. Without this the container boots into
# install mode and the API + admin CLI are both unusable.
FORGEJO__security__INSTALL_LOCK: "true"
FORGEJO__database__DB_TYPE: sqlite3
FORGEJO__database__PATH: /data/gitea/gitea.db
# ROOT_URL must be the URL a BROWSER uses. Every generated link,
# redirect, clone URL and OAuth callback is built from it.
FORGEJO__server__DOMAIN: localhost
FORGEJO__server__ROOT_URL: http://localhost:3000/
# SSH_PORT is the PUBLISHED port (what users connect to).
# SSH_LISTEN_PORT is the IN-CONTAINER port. They differ on purpose.
FORGEJO__server__SSH_DOMAIN: localhost
FORGEJO__server__SSH_PORT: "2222"
FORGEJO__server__SSH_LISTEN_PORT: "22"
volumes:
- forgejo-data:/data
ports:
- "3000:3000"
- "2222:22"
volumes:
forgejo-data:Do not add a version: key — Compose v2 warns the attribute 'version' is
obsolete, it will be ignored. Do not bind-mount /etc/timezone or
/etc/localtime (Pitfall 2); the TZ variable above replaces both portably.
Validate before starting — this parses the file and starts nothing:
docker compose -f docker-compose.yml configStep 2: Start Forgejo
docker compose up -dFirst boot takes roughly 10-30 seconds to create the SQLite schema. Wait for the health endpoint rather than guessing:
# /api/healthz needs no auth and works even on a private instance
curl -s http://localhost:3000/api/healthzA configured instance returns "status": "pass" with a database:ping check.
Now confirm it is not stuck in install mode — this is the check that matters:
curl -s http://localhost:3000/ | grep -qi '<title>Installation' \
&& echo "STILL IN INSTALL MODE — INSTALL_LOCK not applied" \
|| echo "installed OK"/api/healthz returns 200 even in install mode, so it proves the process is
alive, not that setup succeeded. The title check is what distinguishes them.
Step 3: Create the admin user and its API token
With INSTALL_LOCK=true there is no wizard and no "first registered user becomes
admin" step — you create the admin directly. One command creates the user and
prints an API token:
docker exec -u git forgejo forgejo admin user create \
--username <admin-name> \
--email <admin-email> \
--password '<password>' \
--admin \
--must-change-password=false \
--access-token \
--access-token-name cli \
--access-token-scopes "write:repository,write:issue,write:user"It prints New user '<admin-name>' has been successfully created! followed by
Access token was successfully created... <token>. Store that token now — it
is not shown again.
-u git is required: the Forgejo process and everything under /data are owned
by the git user, and the CLI must run as that user.
Need another token later:
docker exec -u git forgejo forgejo admin user generate-access-token \
--username <admin-name> --token-name ci \
--scopes "write:repository,write:issue,write:user"Scope note: POST /api/v1/user/repos (Step 4) requires write:user, not
write:repository. A token scoped only to repositories is rejected with
token does not have at least one of required scope(s): [write:user].
Step 4: Create your first repo
Via web UI: log in at http://localhost:3000, click "+" → New Repository.
Via API — returns 201 Created:
curl -s -o /dev/null -w '%{http_code}\n' \
-X POST "http://localhost:3000/api/v1/user/repos" \
-H "Authorization: token <token>" \
-H "Content-Type: application/json" \
-d '{"name":"my-project","description":"My project","private":false,"auto_init":true}'Step 5: Push code to Forgejo
cd my-project
git init
git add -A
git commit -m "Initial commit"
# HTTPS with a token
git remote add forgejo http://<admin-name>:<token>@localhost:3000/<admin-name>/my-project.git
git push -u forgejo mainEmbedding the token in the remote URL writes it to .git/config in plaintext.
For anything long-lived use a credential helper or the SSH remote below instead.
Step 6: (Optional) Set up CI with Forgejo Actions
Get a registration token from Site Administration → Actions → Runners → Register New Runner.
Register once (a one-shot container that exits), then run the daemon:
# 1. Register — this writes .runner into the volume, then exits.
docker run --rm \
-v forgejo-runner-data:/data \
-w /data \
codeberg.org/forgejo/runner:latest \
forgejo-runner register --no-interactive \
--instance http://host.docker.internal:3000 \
--token <runner-registration-token> \
--name local-runner
# 2. Run the daemon — this is the long-lived container.
docker run -d --name forgejo-runner --restart unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock \
-v forgejo-runner-data:/data \
-w /data \
--add-host host.docker.internal:host-gateway \
codeberg.org/forgejo/runner:latest \
forgejo-runner daemonregister and daemon are two different commands. Running register with
-d and then docker start re-runs registration every boot and never starts a
daemon — the runner will never appear as online.
Then add .forgejo/workflows/ci.yml to a repo:
name: CI
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "CI is running"SSH Git Access
The container listens on 22 internally and is published on 2222. Because
Step 1 sets SSH_PORT: "2222", the UI and API hand out clone URLs carrying the
published port:
ssh://git@localhost:2222/<admin-name>/my-project.gitgit remote add forgejo ssh://git@localhost:2222/<admin-name>/my-project.gitAdd your public key first under Settings → SSH / GPG Keys, or the push fails with
Permission denied (publickey).
Optional ~/.ssh/config entry — scope it to a dedicated alias rather than
Host localhost, which would hijack every other SSH connection to localhost:
Host forgejo-local
HostName localhost
Port 2222
User gitThen the remote becomes forgejo-local:<admin-name>/my-project.git.
Backup
Use Forgejo's own dump command. It quiesces the database and produces a single
consistent archive — tar-ing a live SQLite file can capture a torn write:
docker exec -u git forgejo forgejo dump --type tar.gz --file /data/backup.tar.gz
docker cp forgejo:/data/backup.tar.gz ./forgejo-backup-$(date +%Y%m%d).tar.gz
docker exec -u git forgejo rm /data/backup.tar.gzIf you must snapshot the raw volume instead, stop the container first, and note the Windows path caveat in Pitfall 9:
docker compose stop forgejo
MSYS_NO_PATHCONV=1 docker run --rm -v forgejo-data:/data -v "$(pwd)":/backup \
alpine tar czf /backup/forgejo-volume-$(date +%Y%m%d).tar.gz /data
docker compose start forgejoCommon Pitfalls
:latestdoes not exist.codeberg.org/forgejo/forgejo:latestfails the pull withmanifest unknown— Forgejo publishes nolatesttag. Use a major line (:15) or an exact version (:15.0.3). Pinning also matters because a major upgrade migrates the database irreversibly; there is no downgrade path once it runs.- Do not bind-mount
/etc/timezoneand/etc/localtime. Nearly every Forgejo guide copies these two lines from a Linux-host example. On Windows the daemon fails the container withError response from daemon: mkdir C:\Program Files\Git\etc\timezone: Access is denied, because there is no such host path. SetTZinstead — it works on every host. - Boot without
INSTALL_LOCKand the instance is inert. The container serves the install wizard at/(not at/install, which 404s), and every other route is swallowed by the installer:/api/v1/versionreturns a404HTML page, andforgejo admin user createaborts with[F] Unable to load config file for a installed Forgejo instance. SettingFORGEJO__security__INSTALL_LOCK=trueplus the database settings skips it. ROOT_URLis the single most common misconfiguration. It is not cosmetic — redirects after login, asset URLs, clone URLs, webhook payloads and OAuth callbacks are all generated from it. If you remap the host port to3001, or front the instance with a reverse proxy athttps://git.example.com/,ROOT_URLmust be changed to exactly that browser-facing URL. Left athttp://localhost:3000/behind a proxy, users log in and get bounced tolocalhost:3000, and assets fail to load.SSH_PORTvsSSH_LISTEN_PORT.SSH_LISTEN_PORTis where sshd listens inside the container (22);SSH_PORTis the port that goes into the clone URLs the UI shows. Publishing2222:22while leavingSSH_PORTat its22default makes the UI advertisegit@host:<repo>.git, which clients try on port 22 and fail. SetSSH_PORTto the published port.- Anonymous API access may be disabled. On an instance with
REQUIRE_SIGNIN_VIEW = true(the common choice for a private server),curl http://localhost:3000/api/v1/versionreturns403 {"message":"Only signed in user is allowed to call APIs."}— the server is perfectly healthy. Use/api/healthzfor unauthenticated liveness, or send the token with the request. - SQLite is fine for one person; switch on concurrency, not size. SQLite
holds a single writer lock, so concurrent CI runners plus web users produce
database is lockederrors. Move to PostgreSQL when you add runners or users, by swapping theFORGEJO__database__*variables — but do it on a fresh instance or viaforgejo dump/restore; changingDB_TYPEon a running instance does not migrate existing data. - Runner can't reach the instance. Inside the runner container
localhostis the runner itself. Usehttp://host.docker.internal:3000(adding--add-host host.docker.internal:host-gatewayon Linux Engine), or put both containers on one Compose network and use the service name. - Windows/Git Bash mangles POSIX paths in
dockerarguments. Any argument starting with/is rewritten to a Windows path by MSYS, so-v $(pwd):/backupsilently mounts somewhere unexpected and the backup file never appears where you asked for it. Prefix the command withMSYS_NO_PATHCONV=1, or write the source as"/$(pwd)"with a leading slash. PowerShell and Linux/macOS shells are unaffected. --must-change-passworddefaults to forcing a reset. An admin created without--must-change-password=falsemust change its password at first web login, and API calls before that can be rejected. Pass the flag explicitly.- Port 3000 collides constantly. If you remap to
3001:3000, changeROOT_URLtohttp://localhost:3001/in the same edit — remapping the port alone leaves every generated link pointing at 3000.
Verification Checklist
docker compose -f docker-compose.yml configparses with noversionobsolete warningdocker compose psshows the containerrunningcurl -s http://localhost:3000/api/healthzreports"status": "pass"includingdatabase:pingcurl -s http://localhost:3000/ | grep -i '<title>Installation'returns nothing — the instance is past the install wizard, not merely aliveforgejo admin user createprinted both the user confirmation and an access token- The token creates a repo:
POST /api/v1/user/reposreturns201(not403 ... [write:user]) GET /api/v1/repos/<user>/<repo>shows aclone_urland anssh_urlwhose host and port match how you actually reach the server — notlocalhost:3000when you browse it at another address- Admin login succeeds in a real browser, and a page reload keeps you logged in (a wrong
ROOT_URLshows up here as a redirect bounce) - A test repo pushes over the configured remote (HTTPS token or SSH on the published port)
- If Actions was configured: the runner shows online under Site Administration → Actions → Runners, and a sample workflow run succeeds
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

