Why Choose Forgejo for Your Self-Hosted Git Forge
Forgejo was born in 2022 as a community fork of Gitea, driven by concerns about the project's governance. Its development is entirely community-led, licensed under GPLv3+ since v9.0 (August 2024; MIT before that), with no dependency on a commercial entity. For teams wanting full control over their source code, CI/CD pipelines, and artifacts without relying on GitHub, GitLab, or Bitbucket, Forgejo is one of the lightest options available. The interface will feel familiar to any GitHub user, migration from Gitea is seamless, and GitHub Actions compatibility lowers the barrier to adoption.
Concrete Benefits of a Self-Hosted Forgejo
- Full sovereignty: your code never leaves your infrastructure
- GitHub Actions compatible via Forgejo Actions — reuse existing workflows
- Low memory footprint: a small VPS is enough for a small team
- Built-in package registry: npm, PyPI, Maven, Helm, OCI container
- No limits on private repositories or collaborators
- ActivityPub federation in progress — future interoperability with other forges
- Open-source, auditable, no pricing surprises
- Controlled updates: you decide when to upgrade
Hardware and Software Requirements with Real Numbers
Size your VPS according to your actual team size before deploying Forgejo. For 1–5 developers with a few dozen repositories, 1 vCPU and 1 GB RAM are sufficient. For 5–20 developers with active CI/CD, plan for 2 vCPU and 2 GB RAM. Beyond 20 developers or for large repositories (monorepos, binaries), 4 vCPU and 4 GB RAM ensure a smooth experience. For storage, account for your Git repository volume plus 20% headroom for artifacts and backups.
On the network side, three ports must be open in your firewall: port 80 (HTTP, for Let's Encrypt validation), port 443 (HTTPS, web traffic), and port 2222 (Git SSH, to avoid conflicting with the admin SSH on port 22). On the software side, you need Docker Engine ≥ 24, Docker Compose v2, and a domain name pointing to your VPS.
Deploy Forgejo with Docker and SSL
Prepare the environment
Create the directories that will persist data outside the container:
mkdir -p /opt/forgejo/{data,config,db} chown -R 1000:1000 /opt/forgejoCreate
/opt/forgejo/.envwith your values:FORGEJO_DOMAIN=git.yourdomain.com FORGEJO_SSH_PORT=2222Add a generated database password, never a sample one, then protect the file:
echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)" >> /opt/forgejo/.env chmod 600 /opt/forgejo/.envCreate the docker-compose.yml
services: forgejo: image: codeberg.org/forgejo/forgejo:17 restart: unless-stopped environment: - USER_UID=1000 - USER_GID=1000 - FORGEJO__database__DB_TYPE=postgres - FORGEJO__database__HOST=db:5432 - FORGEJO__database__NAME=forgejo - FORGEJO__database__USER=forgejo - FORGEJO__database__PASSWD=${POSTGRES_PASSWORD} volumes: - /opt/forgejo/data:/data ports: - "127.0.0.1:3000:3000" - "${FORGEJO_SSH_PORT:-2222}:22" depends_on: - db db: image: postgres:16-alpine restart: unless-stopped environment: - POSTGRES_DB=forgejo - POSTGRES_USER=forgejo - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} volumes: - /opt/forgejo/db:/var/lib/postgresql/dataStart the services
cd /opt/forgejo docker compose up -d docker compose logs -f forgejoWait until the logs show the web server listening on port 3000. Forgejo runs its schema migrations automatically on first start.
Configure the reverse proxy and SSL with Caddy
Caddy is the simplest option for automatic Let's Encrypt certificates. Install it and create
/etc/caddy/Caddyfile:git.yourdomain.com { reverse_proxy localhost:3000 }Start Caddy:
systemctl enable --now caddy. The TLS certificate is issued and renewed automatically. If you prefer nginx, configure a standardproxy_pass http://127.0.0.1:3000;block with Certbot.Complete the instance setup
Open
https://git.yourdomain.comin your browser. The setup wizard appears once only. Fill in the domain, the SSH URL (ssh://git.yourdomain.com:2222), the admin email address, and disable public registration on this screen if your forge is for private use. Once the wizard is submitted, the configuration is locked in/opt/forgejo/data/gitea/conf/app.ini.Install a Forgejo Actions runner
The runner executes your workflows' code: install it on a dedicated machine if you can. In *Site Administration → Actions → Runners* (
/admin/actions/runners), click *Create new runner*: Forgejo displays a UUID and a token. Generate the configuration file (docker run --rm data.forgejo.org/forgejo/runner:13 forgejo-runner generate-config > data/runner-config.yml), copy the instance URL, the UUID and the token into theserver.connectionssection, and declare your labels (for exampleubuntu-latest:docker://node:20) in therunnersection. Then deploy the runner with its own Docker daemon (docker-in-docker) instead of mounting the host's/var/run/docker.sock, which would give every job root access to the server:services: docker-in-docker: image: docker:dind privileged: true command: ["dockerd", "-H", "tcp://0.0.0.0:2375", "--tls=false"] restart: unless-stopped runner: image: data.forgejo.org/forgejo/runner:13 depends_on: - docker-in-docker environment: DOCKER_HOST: tcp://docker-in-docker:2375 user: "1001:1001" volumes: - ./data:/data command: forgejo-runner daemon --config runner-config.yml restart: unless-stoppedThe docker-in-docker daemon's port 2375 stays on Compose's internal network: never publish it. The runner appears in the interface as soon as it connects. Each label sets the execution mode: docker:// (container, recommended), lxc:// (LXC container) or host (directly on the machine, with no isolation at all: keep it for trusted tasks).
Writing Your First Forgejo Actions Workflows
Compatibility with GitHub Actions syntax is broad. Create .forgejo/workflows/ci.yml at the root of your repository:
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: |
npm ci
npm testThe runs-on labels correspond to the labels declared when registering the runner. You can define multiple runners with different labels (e.g., arm64, gpu, high-memory) and target the right environment per job. Artifacts and caching work with the same official actions as on GitHub.
Post-Install Security: Essential Settings
A freshly installed Forgejo is functional but not hardened. Here are the six points to address before opening the forge to your team.
Disable public registration. If you did not do it in the wizard, add FORGEJO__service__DISABLE_REGISTRATION=true to the container's environment variables (or DISABLE_REGISTRATION = true in the [service] section of app.ini): only an administrator can then create accounts. On an internal forge, nobody should be able to create an account without an invitation.
Enable 2FA for administrators. Every admin account must enable two-factor authentication under *Settings → Security*. To enforce it, set GLOBAL_TWO_FACTOR_REQUIREMENT in the [security] section of app.ini (admin for administrators, all for every user). The REQUIRE_SIGNIN_VIEW setting only forces users to sign in to view the forge.
SSH keys only. Disable password authentication in the host server's /etc/ssh/sshd_config (PasswordAuthentication no). For Git SSH (port 2222), Forgejo only accepts public keys registered in user profiles — this is its default behavior.
Webhook secrets. When creating each webhook (to external CI, Slack, etc.), always fill in the *Secret* field. Forgejo signs the payload with HMAC-SHA256; your receiver must verify this signature before processing the event.
Private packages. If you use the built-in package registry, remember that a package inherits its owner's visibility: publish it under a private organization or account to restrict it to members. Access tokens for publishing should have minimal scope (write:package only, never a global admin token).
Firewall. Close all ports except 80, 443, and 2222. Port 3000 (internal Forgejo) must never be exposed directly; it is consumed only by the reverse proxy on loopback, which is why the compose file publishes it on 127.0.0.1:3000. A port published by Docker bypasses ufw rules: only this 127.0.0.1 binding keeps it off the Internet.
Updating Forgejo Without Downtime
The Docker update procedure is straightforward and requires only a few seconds of unavailability.
cd /opt/forgejo
# 1. Pull the new image
docker compose pull forgejo
# 2. Back up the database before any migration
docker compose exec db pg_dump -U forgejo forgejo > backup-$(date +%Y%m%d).sql
# 3. Restart the service
docker compose up -d forgejo
# 4. Verify schema migrations
docker compose logs forgejo | grep -i migratForgejo runs its migrations automatically on startup. Check that the logs report no migration error. In case of a migration error, restore the SQL backup and report the issue on the Forgejo tracker. The 17 tag only follows 17.x releases: to move to the next major version, change the tag in the compose file after a full backup, and always read the release notes before jumping major versions. Forgejo refuses to start on a database newer than itself: rolling back means restoring the backup.
Migrate from Gitea in Five Minutes
Forgejo is a direct fork of Gitea: upgrading from Gitea is supported up to and including version 1.22, going through Forgejo v10.0.x first. To migrate, stop Gitea, copy its data directory (/data or /opt/gitea) to /opt/forgejo/data, replace the image with codeberg.org/forgejo/forgejo:10 in your docker-compose, restart, then upgrade to the current version. Forgejo automatically detects the Gitea schema and applies its own migrations. Your repositories, users, SSH keys, webhooks, and issues are fully preserved. Gitea versions after 1.22 are not covered by Forgejo's official upgrade guide: do not attempt a direct switch.
Troubleshooting: Most Common Errors
SSH connection refused on port 2222. First verify the port is published by Docker (docker compose ps → Ports column). Then test from the client: ssh -p 2222 [email protected]. If the response is PTY allocation request failed, the connection is working — Forgejo responds Hi <user>! You've successfully authenticated. If you get Connection refused, the firewall is blocking the port: ufw allow 2222/tcp.
Runner showing offline in the interface. The runner contacts Forgejo over outbound HTTPS. Verify the runner container can resolve and reach git.yourdomain.com. A self-signed certificate requires mounting your CA in the container. Restart the runner after fixing connectivity: docker compose restart runner.
Push rejected by a pre-receive hook. Forgejo may reject a push if a branch protection rule is active (code review required, mandatory CI tests) or if a file size rule is exceeded. The git error message contains the details. For large binaries (> 100 MB), use Git LFS: Forgejo supports the LFS protocol natively.
Slow interface after several months. Run the Git garbage collector on active repositories from the admin panel: *Site Administration → Dashboard → Maintenance operations*, repository garbage collection task. To automate this, enable the [cron.git_gc_repos] scheduled task in app.ini (ENABLED = true; it runs every 72 h by default).
ActivityPub Federation: What Forgejo Is Building
Forgejo is building forge federation with ForgeFed, an extension of the ActivityPub protocol (the same one used by Mastodon). It remains experimental and disabled by default ([federation] ENABLED = false): moderation and access control are not developed yet, and the project warns that an incompatible change can "burn" the instance's domain for any future federation. Do not enable it on a production forge. Full federation — cross-instance forking, inter-forge pull requests — is being standardized within the ForgeFed working group. If your collaborators are on different forges, this evolution will eventually allow contributing without creating an account on each instance.
Forgejo vs Gitea vs GitLab CE: Choosing Your Forge
Scroll the table
| Criterion | Forgejo | Gitea | GitLab CE |
|---|---|---|---|
| Governance | Community (Codeberg e.V.) | Commercial (CommitGo) | Commercial (GitLab Inc) |
| Minimum RAM | Low (small VPS) | Low (small VPS) | 8 GB (16 GB recommended) |
| GitHub Actions compatibility | Yes (Forgejo Actions) | Yes (Gitea Actions) | No (GitLab CI, own syntax) |
| Package registry | Yes (multi-format) | Yes | Yes |
| ActivityPub federation | In progress (ForgeFed) | No | No |
| Migration from GitHub | Yes (mirror + import) | Yes | Yes |
| Admin interface | Simple, built-in | Simple, built-in | Full-featured but heavy |
| License | GPLv3+ (since v9.0) | MIT | MIT (CE) / EE (paid) |
| Release frequency | Quarterly (+ LTS) | Several per year | Monthly |