Self-host Forgejo
Last updated:
Codebahn runs on Forgejo. If you want to run your own instance, this page covers the decisions you need to make, the order to make them in, and where to find the details. It works whether you are migrating from Codebahn, from another hosted service, or starting from scratch.
Forgejo’s own documentation is thorough. This guide does not duplicate it. It connects the pieces into a single path and covers the things that upstream docs tend to skip: what to think about before you start, what matters for a small team, and what you will need to recreate if you are moving from a hosted service.
The stack
Section titled “The stack”A working Forgejo setup with CI has five parts:
- Forgejo itself (the application)
- A database (PostgreSQL for production, SQLite to experiment)
- A reverse proxy with TLS (Caddy is the simplest)
- Email for notifications (any SMTP provider)
- A runner for CI (the
forgejo-runnerbinary, running jobs in Docker)
A single Linux server with 2 CPU cores, 4 GB RAM, and an SSD handles 5-20 active users comfortably. Forgejo is lightweight; most of the disk goes to your repositories and CI artifacts.
Install Forgejo
Section titled “Install Forgejo”Docker Compose is the fastest path. The official image is codeberg.org/forgejo/forgejo. A rootless variant is available.
For bare metal, download the binary from the Forgejo releases page. Create a git system user, set up the directory structure, and install the systemd unit from the contrib/ directory in the Forgejo source.
Database
Section titled “Database”PostgreSQL is the production choice. Forgejo runs migrations automatically on startup, so initial setup is creating the database and user.
SQLite is fine for trying things out. It requires no setup at all. For anything you care about keeping, use PostgreSQL; it handles concurrent access, backups, and replication in ways SQLite does not.
Reverse proxy and TLS
Section titled “Reverse proxy and TLS”Put Forgejo behind a reverse proxy. Caddy provisions and renews TLS certificates from Let’s Encrypt automatically, with no configuration beyond the domain name. Nginx works too, with certbot for certificates.
Two things that trip people up: Forgejo needs ROOT_URL in app.ini set to the public URL (with the scheme), and Nginx needs merge_slashes off to avoid breaking URL-encoded paths.
Forgejo sends notifications, password resets, and registration confirmations over SMTP. Any provider works: a transactional email service (Mailgun, Postmark, Scaleway TEM), your own mail server, or a relay through Gmail. Configure the [mailer] section in app.ini.
Set up CI
Section titled “Set up CI”Forgejo Actions is built into Forgejo. Enable it in app.ini:
[actions]ENABLED = trueThen install and register a runner. The forgejo-runner binary polls your Forgejo instance for jobs, runs them in Docker containers, and reports the results back.
The runner needs Docker (or Podman) on the machine where it runs. For isolation, run a dedicated Docker-in-Docker container alongside the runner rather than mounting the host Docker socket.
Registration: create a runner in the Forgejo UI (Settings > Actions > Runners), which gives you a UUID and token. Add those to the runner’s config file and start the daemon. Runners can be scoped to an instance, an organization, or a single repository.
Forgejo reads workflow files from .forgejo/workflows/, .gitea/workflows/, and .github/workflows/. Most GitHub Actions workflows run without changes, but Forgejo Actions is not designed to be fully compatible. Check the Forgejo Actions documentation for the current list of differences.
What works and what does not
Section titled “What works and what does not”Most GitHub Actions workflows run without changes. Caching (actions/cache), setup actions (setup-node, setup-go), and artifact upload/download all work. The runner ships with a built-in cache server.
Some things differ. Actions are resolved from data.forgejo.org by default, not github.com, though you can configure the fallback. Some workflow keys behave differently or are not supported. Check the Forgejo Actions documentation for the current list.
Bring your data
Section titled “Bring your data”If you are migrating from a hosted Forgejo or Gitea service, there are three ways to move your repositories:
Organization export (if your host offers it): download an archive containing Git data, issues, pull requests, labels, milestones, releases, comments, and reviews. Import each repository with Forgejo’s restore-repo command.
API migration: use POST /api/v1/repos/migrate on your new instance, pointing it at the source. This pulls Git data, issues, PRs, labels, milestones, releases, and wiki in one call. Works across Forgejo, Gitea, GitHub, and GitLab.
Git clone and push: clone with --mirror, push to the new remote. This moves the Git history only. Use the API or a dump/restore for metadata.
Coming from Codebahn
Section titled “Coming from Codebahn”If you are migrating from Codebahn, the path is the organization export. Before you start setting up the new instance, do the following while you still have access to Codebahn.
Export your org. Go to Settings > Export in your organization on Codebahn. This produces a tar.gz archive containing Git data (all branches, tags, refs), issues, pull requests with reviews and inline comments, labels (repo and org level), milestones, releases with assets, topics, and your team structure with member assignments.
Capture what the export does not include. Use the Codebahn API to build your manifest while the instance is still available:
- CI variables (names and values; unlike secrets, these are readable):
GET /api/v1/repos/{owner}/{repo}/actions/variables - CI secret names (values are encrypted and cannot be read; you need them from your own records):
GET /api/v1/repos/{owner}/{repo}/actions/secrets - Webhooks (URL, events, content type):
GET /api/v1/repos/{owner}/{repo}/hooks - Deploy keys:
GET /api/v1/repos/{owner}/{repo}/keys - The same endpoints exist at org level for org-scoped secrets and variables
Pull container images. If you publish images to Codebahn’s container registry, pull them before leaving: docker pull codebahn.net/{org}/{image}:{tag}. Re-push to the new instance’s registry, or rebuild from source once your CI pipeline is running on the new host.
Check your runner labels. Open your workflow files and look at the runs-on: values. Register your self-hosted runner with matching labels so workflows run without changes. If you used Codebahn’s hosted runner labels, map them to whatever labels you assign your own runner.
Match the Actions resolution setting. Codebahn resolves Actions from GitHub by default. To keep the same behavior on your self-hosted instance, set DEFAULT_ACTIONS_URL = github in app.ini. Without this, Actions resolve from data.forgejo.org and you may need to update action references (e.g. actions/checkout) in your workflow files.
Restore. Extract the archive and import each repository with Forgejo’s restore-repo command. The team structure from the manifest needs to be recreated manually on the new instance: create the org, create user accounts, then assign teams and permissions.
Update your remotes. Every developer on the team needs to update their Git remote URLs. Any external integrations pointing at Codebahn (CI webhooks from other services, deployment triggers, monitoring) need the new address.
What comes with any Forgejo export
Section titled “What comes with any Forgejo export”Repositories (all branches, tags, refs), issues, pull requests with reviews and inline comments, labels (repo and org level), milestones, releases with assets, and topics.
What you will recreate by hand
Section titled “What you will recreate by hand”Some things are tied to the service and can’t be included in an export. This is true of any hosted service. The list below is the complete inventory. If you captured a manifest from your previous host using the API calls above (or your host provides one), this is what each item maps to.
CI secrets. Stored encrypted; values cannot be extracted from any host. You will need the values from your own records (a password manager, a vault, a shared document). On the new instance, set them via Settings > Actions > Secrets or the API. The manifest tells you which secrets existed and which repositories used them.
CI variables. Unlike secrets, variables are not encrypted and can be read from the API of most Forgejo hosts. If you have API access to the old instance, script the migration. If not, the manifest has the names and values.
Deploy keys. The public key was stored on the old host; the private key is on whatever machine does the deploying. Re-add the same public key to the new instance via Settings > Deploy Keys or the API.
Webhooks. The URL, events, and content type are in the manifest. Any authentication headers or webhook secrets need to be re-entered. If the webhook points at an external service (Slack, a deployment system), update that service’s configuration to trust the new source IP or domain.
Runner registrations. Runners are registered per-instance. Generate new registration tokens on the new instance and register your runners again. The labels you used (which control which workflows run on which runners) should match.
Container and package registry images. If you pushed images to the old host’s container registry, those images are not part of the Git export. Pull them before leaving, re-push to the new instance’s registry, or rebuild from source using your CI pipeline. The same applies to any packages (npm, Maven, Go modules) published to the old host’s package registry.
OAuth2 applications. If external services use the old host as an OAuth provider, you will need to create new OAuth2 applications on the new instance and update the client ID and secret in each service.
Branch protection rules. These are instance-level settings, not stored in the repository. Recreate them via Settings > Branches or the API: required reviews, status checks, force-push restrictions.
Team and organization permissions. If you used the organization export, the team structure is in the manifest. Members need accounts on the new instance before you can assign them. If you used API migration, set up teams and permissions manually.
Two-factor authentication. 2FA secrets are per-instance. Users will need to re-enroll on the new instance.
Notification preferences, watched repositories, starred items. Per-user, per-instance. Not critical, but worth knowing.
Git LFS
Section titled “Git LFS”If your repositories use Git LFS, the pointer files are in the Git history but the large file objects are stored separately. When migrating via the API with lfs: true, Forgejo pulls the objects from the source. When migrating via git clone, fetch LFS objects explicitly before pushing:
git lfs fetch --allgit lfs push --all <new-remote>Keep it running
Section titled “Keep it running”Backups
Section titled “Backups”Back up the database (PostgreSQL dump), the repository directory, LFS storage, and app.ini. Forgejo has a built-in dump command that bundles everything into one archive, or you can back up each part separately for more control.
Test your restores. A backup you have never restored is a backup you do not have.
Updates
Section titled “Updates”Forgejo uses major.minor.patch versioning. Within a major version, updates are safe to apply. The process is: stop Forgejo, replace the binary (or pull the new image), start Forgejo. Database migrations run automatically on startup.
Back up before every update. If something goes wrong, restore the backup and the previous binary.
Subscribe to the Forgejo releases feed or watch the repository on Codeberg to know when updates are available, especially security patches.
Monitoring
Section titled “Monitoring”Forgejo exposes a health endpoint at /api/healthz. Point an uptime monitor at it. For internal metrics, Forgejo can expose Prometheus metrics via the [metrics] section in app.ini.
Watch disk usage. Repository data and CI artifacts grow over time. Set artifact retention limits in app.ini ([actions] ARTIFACT_RETENTION_DAYS) and run git gc periodically on large repositories.

