# 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. ```sh cp config/forge.example.toml config/forge.toml ``` 2. Start the server. ```sh 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: ```sh 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: ```sh 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: ```sh # 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: ```sh 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. ```sh 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](https://ferron.sh) 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`): ```sh 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`: ```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](#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 `, or `cargo run -p forge-web -- admin ` from a checkout. | Command | What it does | |---|---| | `admin create-user [--admin]` | Create a user. The first user is always an admin. | | `admin set-username ` | Change only the capitalization of a username. | | `admin reset-password ` | Set a random temporary password and sign the user out everywhere. | | `admin delete-user [--yes]` | Delete a user. | | `admin trash-list` | List deleted repos, oldest first. | | `admin trash-purge --older-than-days ` | Permanently remove trashed repos older than N days. | | `admin audit-purge --older-than-days ` | Delete audit log entries older than N days. | | `admin backup --out [--include-key] [--repos-only \| --db-only]` | Write a backup set. | | `admin backup-verify ` | Check a backup set. | | `admin restore --from --data-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: ```sql VACUUM INTO '/var/backups/forge/forge-2026-10-15.db'; ``` For a full backup, use `forge admin backup --out `. See [`docs/backup.md`](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 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`](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/`.