A local GitHub in a container: Forgejo, Gitea, GitLab or plain git
Why a server on your own machine
GitHub is where the code is published. A server on your own machine is for the work before that: branches from AI agents or from yourself, reviewed as pull requests in a browser and merged, without any of it leaving the computer. In the aredel repository that is how agent branches are reviewed, see Claude Code, OpenCode and a human in one repository. How remotes, push and pull work in general is in GitLab and GitHub as remotes.
The options
| Option | What you get | Cost |
|---|---|---|
| A bare repository on disk | a remote to push to and fetch from; no web page, no pull requests | nothing: git init --bare |
| Forgejo | web interface, issues, pull requests with review and merge; CI through Forgejo Actions with a separate runner | tested here: 206 MB image, about 100 MiB of memory when idle, SQLite inside the container |
| Gitea | the project Forgejo was forked from, with the same kind of features | similar to Forgejo; in 2026 its version numbers changed from 1.x to 28.x, so check the tag on Docker Hub before pinning one |
| GitLab Community Edition | a full platform with built-in CI and much more | heavy: the GitLab guide for memory-constrained setups asks for at least 2 GB of RAM plus 1 GB of swap for up to five developers |
A bare repository is enough when one person pulls from agents and nobody needs a review page. For pull requests on one machine, Forgejo is the lightest of the three servers; GitLab pays off only when you need its CI and project features.
Run Forgejo in Docker
Forgejo 17.0 was released on 2026-10-15; the long-term support line is 15.x. The image has no latest tag, so pin the version. This starts it on 127.0.0.1 only, with its data in a named volume, the web installer skipped and self-registration closed:
docker run -d --name forgejo --restart unless-stopped -p 127.0.0.1:3000:3000 -e USER_UID=1000 -e USER_GID=1000 -e FORGEJO__security__INSTALL_LOCK=true -e FORGEJO__server__ROOT_URL=http://127.0.0.1:3000/ -e FORGEJO__service__DISABLE_REGISTRATION=true -v forgejo-data:/data codeberg.org/forgejo/forgejo:17.0.0
- -p 127.0.0.1:3000:3000 publishes the web port to this machine only.
- FORGEJO__SECTION__KEY variables set values in app.ini. INSTALL_LOCK=true skips the installation page, so the server starts on the default SQLite database.
- DISABLE_REGISTRATION=true: accounts are created on the command line only.
- forgejo-data keeps repositories, accounts and the database when the container is removed or upgraded.
It answered after six seconds:
curl -s http://127.0.0.1:3000/api/v1/version
{"version":"17.0.0+gitea-1.22.0"}
And the sign-up page now says:
Registration is disabled. Please contact your site administrator.
An account and a token
Create the first account, an administrator, with the Forgejo command line inside the container, as its git user. With --random-password the password is printed once, and Forgejo asks for a new one at the first login:
docker exec -u git forgejo forgejo admin user create --username demo --email demo@example.com --random-password --admin
New user 'demo' has been successfully created!
For scripts and the API, an access token; --raw prints only the token, which is a password and belongs in a password manager:
docker exec -u git forgejo forgejo admin user generate-access-token --username demo --token-name agents --scopes all --raw
A repository and the first push
Create an empty repository in the web interface, or with the API:
curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" -X POST http://127.0.0.1:3000/api/v1/user/repos -d '{"name":"site","default_branch":"main"}'
201
Add it as a second remote next to GitHub and push. Git asks for the user name and password; a token works as the password:
git remote add local http://127.0.0.1:3000/demo/site.git
git push local main claude/readme
To http://127.0.0.1:13000/demo/site.git * [new branch] main -> main * [new branch] claude/readme -> claude/readme
A pull request, opened and merged
In the web interface: New pull request, base main, compare claude/readme, review the diff, Merge. The same through the API, which is what the test used:
curl -s -H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" -X POST http://127.0.0.1:3000/api/v1/repos/demo/site/pulls -d '{"base":"main","head":"claude/readme","title":"Extends the README"}'
curl -s -H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" -X POST http://127.0.0.1:3000/api/v1/repos/demo/site/pulls/1/merge -d '{"Do":"merge"}'
Results, read back from the API: pull request 1 was open, its only changed file was README.md with one added line, and Forgejo reported it mergeable; after the merge call (HTTP 200) it was closed and merged, and main on the server started with:
1819d84 Merge pull request 'Extends the README' (#1) from claude/readme into main
Tested on 2026-10-23 with Forgejo 17.0.0 on a throwaway container on port 13000 and a dummy repository; the commands above use port 3000, as in the setup that runs for aredel now. The push was run by a person: the deny rules of the repository stop AI agents from running git push, even to a server on the same machine.
Daily use with AI agents
- Each agent works in its own worktree and branch, for example claude/str-move; see A worktree and a branch per agent.
- You push the agent branch to the local server and open a pull request into main.
- You review and merge in the browser, then git pull local main and push main to GitHub.
- Remove the worktree and the branch when the pull request is merged.
Stop, upgrade, back up, remove
docker stop forgejo
docker start forgejo
To upgrade, read the release notes first, since a new major version can need manual steps, then remove the container and run the same command with the new tag; the volume keeps the data. To back up, copy the volume, for example into a tar archive in the current folder (not tested here):
docker run --rm -v forgejo-data:/data -v "$PWD":/backup debian:bookworm tar czf /backup/forgejo-data.tgz -C /data .
To remove everything, container and data:
docker rm -f forgejo && docker volume rm forgejo-data
Checklist
- Pin the image version; Forgejo has no latest tag.
- Publish the port on 127.0.0.1 only, and close self-registration.
- Keep the data in a named volume.
- Create the first account on the command line; treat tokens as passwords.
- Add the server as a second remote; GitHub stays where the code is published.
- Let people push; keep agents to their own branches and worktrees.
Official sources:
- Forgejo v17.0 is available
- Forgejo: installation with Docker
- Forgejo: command-line interface
- Gitea 28.0.0 is released
- GitLab: running in a memory-constrained environment
Article author: Arthur Isaev