forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
12.8 KiB

forge

forge is a self-hosted git forge. You run it on your own server, so your code stays on machines you control and no third-party service sits in the middle. It ships as one binary, forge, which serves the web UI, git over HTTPS, and git over SSH. Repositories are plain bare git repositories on disk, and account data lives in one SQLite database.

Try the live instance at https://git.tahinli.com.

forge is a working name, and it is not released yet.

Features

  • Accounts: local accounts with argon2id passwords, two-factor login (TOTP) with recovery codes, SSH keys, and personal access tokens.
  • Repositories: public or private repos, collaborators, archiving, and a trash for deleted repos.
  • Git: clone and push over HTTPS and SSH. The default branch cannot be deleted or force-pushed, and archived repos reject pushes.
  • Browsing: rendered READMEs, syntax-highlighted files with line links, Markdown source view, commit history and diffs, branches and tags, and .tar.gz or .zip downloads.
  • Collaboration: issues, pull requests with reviews, notifications, and releases.
  • Automation: webhooks, push mirrors, replication to a standby, and CI runners.
  • Large files: Git LFS.
  • Backups: scheduled backup sets with verification and restore.

Quick start

You need Rust (stable, edition 2024) and git 2.38 or newer. forge refuses to start with an older git.

  1. Copy the example config. forge reads config/forge.toml by default.

    cp config/forge.example.toml config/forge.toml
    
  2. Start the server.

    cargo run -p forge-web
    
  3. Open http://127.0.0.1:7660/setup and create the admin account (its password must be at least 12 characters). This page works only while there are no users. You can also create the account from the shell:

    cargo run -p forge-web -- admin create-user tahinli --admin
    

    The command reads the password from FORGE_PASSWORD if it is set, or asks for it twice. Passwords must be at least 12 characters.

  4. Add an SSH key at /settings/keys. Use an ed25519 key:

    ssh-keygen -t ed25519
    

    RSA keys are rejected, because forge turns off SHA-1 signatures.

  5. Create a token at /settings/tokens for HTTPS pushes. It starts with forge_ and is shown only once.

  6. Create a repo at /new, then push to it:

    # SSH
    git remote add origin ssh://git@127.0.0.1:2222/tahinli/my-repo.git
    git push -u origin main
    
    # HTTPS: use your username, and the token as the password
    git remote add origin http://127.0.0.1:7660/tahinli/my-repo.git
    git push -u origin main
    

Account settings live at /settings/security. There you can change your password or email, or turn on two-factor login (save the ten recovery codes it shows). Git over HTTPS uses access tokens, not your password or 2FA codes, so revoke a token at /settings/tokens if it leaks. You can change only the capitalization of your username, at /settings/profile.

Deploy on a server

Run forge behind a TLS reverse proxy for any use beyond localhost. The session cookie is marked Secure, so logging in over plain HTTP will not work outside localhost.

The easiest way is a system service, using the unit in deploy/forge.service. You can also run forge as a dedicated user with a systemd user service. Then enable linger (sudo loginctl enable-linger forge) so it keeps running after logout.

System service setup:

sudo useradd --system --home /var/lib/forge --shell /usr/sbin/nologin forge
sudo install -d -o forge -g forge /var/lib/forge /etc/forge
sudo install -m 0640 -g forge config/forge.example.toml /etc/forge/forge.toml
sudo install -m 0755 target/release/forge /usr/local/bin/forge
sudo install -m 0644 deploy/forge.service /etc/systemd/system/forge.service
sudo systemctl daemon-reload && sudo systemctl enable forge

Before you start it, edit /etc/forge/forge.toml:

  • base_url: your public https:// URL.
  • user_content_base_url: a separate domain, not a subdomain of base_url (see below).
  • ssh_public_host and ssh_public_port: your public host name and port.
  • database and data_dir: absolute paths under /var/lib/forge.
  • trusted_proxies = ["127.0.0.1"] if the reverse proxy runs on the same host. Without this, every client shares the proxy's IP for login throttling.
  • [backup] dir: under /var/lib/forge or /var/backups/forge.

Create the admin account (password at least 12 characters) before the reverse proxy makes the site public. /setup lets the first visitor claim the admin account while there are no users.

sudo -u forge FORGE_CONFIG=/etc/forge/forge.toml /usr/local/bin/forge admin create-user tahinli --admin
sudo systemctl start forge

Open the SSH port (2222 by default) in your firewall. The reverse proxy does not forward SSH, so forge serves it directly.

Reverse proxy

Git needs a proxy that does not buffer and allows long silent periods, because git upload-pack can pause while it builds a pack. This example uses Ferron 3.0.0-rc.7, a pre-release. Ferron uses its own ferron.conf format. A bare host block gets automatic TLS.

forge.example.com {
    buffer_request 0
    buffer_response 0
    proxy {
        upstream http://127.0.0.1:7660 {
            idle_timeout "3600s"
        }
    }
}

usercontent.example.net {
    buffer_request 0
    buffer_response 0
    proxy {
        upstream http://127.0.0.1:7661
    }
}

Set base_url = "https://forge.example.com" and user_content_base_url = "https://usercontent.example.net" to match. Turn off compression on git paths too. forge does not compress responses yet.

User content domain

Raw repo files are served from user_content_base_url, which must be a different domain from base_url (for example usercontent.example.net next to forge.example.com). Browsers then treat it as a separate site, so a file a user uploads cannot run script against your app. The raw origin reads no cookies and sends risky file types as downloads. Links to private repo files carry a ?token= that expires after 10 minutes.

Automatic updates

forge can update itself. When CI passes on a push to the forge repo, the server swaps in the new binary and restarts. Runners do the same when they are idle. This needs the user units in deploy/user/, because the binary lives in ~/bin. Use forge for the server and ci for a runner, each a dedicated user with linger enabled. The system unit deploy/forge.service is for a root-managed install with the binary in /usr/local/bin, which the forge user cannot write, so it does not self-update. Use it or the user units, not both.

As the forge user, install the server units (the runner host does the same as ci, with forge-runner.service and forge-runner-rollback.service):

install -d ~/bin ~/.config/systemd/user
install -m 0755 target/release/forge ~/bin/forge
install -m 0755 deploy/user/forge-rollback ~/bin/forge-rollback
install -m 0644 deploy/user/forge.service deploy/user/forge-rollback.service ~/.config/systemd/user/
systemctl --user daemon-reload && systemctl --user enable --now forge.service

Turn on updates in forge.toml:

[self_update]
enabled = true
repo = "Tahinli/forge"
branch = "main"
job = "build"
artifact = "dist_forge"

Runners update by default (auto_update = true in runner.toml). Set it to false to pin a runner's binary by hand.

If a new binary crashes on start three times within a minute, systemd gives up and runs forge-rollback. That script keeps the old binary as ~/bin/forge.prev, so it puts it back, records the bad build's sha256 in ~/bin/forge.bad, and starts the unit again. A forge or runner skips any hash listed there, so it will not install the same build again. Check the journal (journalctl --user -u forge -e) to see what happened.

Rollback does not undo database migrations. If the new version migrated the database, the old binary runs against the new schema. Take a backup before you rely on updates (see Data and backups).

Configuration

Settings live in TOML. config/forge.example.toml lists every key with comments. Relative paths are relative to the working directory. The keys you are most likely to change:

Key Example Meaning
listen "127.0.0.1:7660" Address for the web UI and git over HTTPS.
base_url "https://forge.example.com" Public URL used in links and clone addresses.
user_content_base_url "https://usercontent.example.net" Public URL of the raw file origin.
user_content_listen "127.0.0.1:7661" Address of the raw file listener.
ssh_listen "0.0.0.0:2222" Address for git over SSH.
ssh_public_host / ssh_public_port "forge.example.com" / 2222 Host and port shown in SSH clone addresses.
registration "closed" closed, invite, or open. Accounts come from /setup, the CLI, or /admin/users.
trusted_proxies ["127.0.0.1"] Proxy addresses whose X-Forwarded-For header is trusted. Leave empty if there is no proxy.

The config file is config/forge.toml by default. Pick another path with --config or the FORGE_CONFIG variable.

Admin commands

Run forge admin <command>, or cargo run -p forge-web -- admin <command> from a checkout.

Command What it does
admin create-user <name> [--admin] Create a user. The first user is always an admin.
admin set-username <current> <NewCase> Change only the capitalization of a username.
admin reset-password <name> Set a random temporary password and sign the user out everywhere.
admin delete-user <name> [--yes] Delete a user.
admin trash-list List deleted repos, oldest first.
admin trash-purge --older-than-days <N> Permanently remove trashed repos older than N days.
admin audit-purge --older-than-days <N> Delete audit log entries older than N days.
admin backup --out <DIR> [--include-key] [--repos-only | --db-only] Write a backup set.
admin backup-verify <PATH> Check a backup set.
admin restore --from <SET> --data-dir <NEW_DIR> Restore a backup set into a new data directory.
admin fsck [--full] Run git fsck on every repo and check the database.
admin lfs-gc Delete unused LFS objects that are at least a day old.

serve is the default command. hook and runner are for git hooks and CI runners. Set RUST_LOG to change the log level. GET /healthz returns ok and the build commit.

Data and backups

Everything lives under data_dir:

Path What it is
forge.db Metadata: users, sessions, keys, tokens, repos.
forge.key Instance key. It encrypts stored secrets. Created on first boot.
ssh_host_ed25519_key SSH host key. Created on first boot.
repos/ Bare repositories.
trash/ Repos deleted from their settings page, until you purge them.

Back up forge.key separately, and keep it offline. Without it, encrypted values such as mirror credentials cannot be read again.

Never copy a live database file with cp or rsync. Take a consistent copy with VACUUM INTO, which is safe while forge runs:

VACUUM INTO '/var/backups/forge/forge-2026-10-15.db';

For a full backup, use forge admin backup --out <DIR>. See docs/backup.md for what a backup set contains, how to ship it off-site, how to restore, and the quarterly restore drill.

Recovering a deleted repo

Deleted repos stay in the trash until admin trash-purge runs. Find one with forge admin trash-list, then clone it with git clone --mirror <path> recovered.git. This restores all branches and tags, but not the owner, visibility, description, or collaborators.

CI runners

CI workflows are TOML files in .forge/ci.toml in your repo. Jobs run on runners, which are forge runner processes on machines you control. The forge never connects to a runner; runners pull jobs over HTTPS. Each job runs in its own rootless sandbox, with its own workspace and a read-only token, and no container tool is needed. See docs/ci.md for the workflow format, the security model, and runner setup.

Security

  • Passwords use argon2id. Tokens are stored as SHA-256 hashes.
  • Private repos you cannot see return 404, so their existence is not revealed.
  • No shell runs for git, and pushes are checked for object integrity. Users cannot add hooks or repo-level git config.
  • Login is throttled: 10 failed attempts per username and 30 per IP, over 15 minutes.
  • Run forge as a dedicated user, keep the git binary patched, and keep forge.key and data_dir private.

License

forge is free software, licensed under the GNU Affero General Public License, version 3 or later (AGPL-3.0-or-later).

The web UI bundles the Figtree and IBM Plex Mono fonts, both under the SIL Open Font License 1.1. Licenses and checksums are in style/fonts/.