# gitea-selfhost Hosted instance: **https://gitea.seamtro.ai** (open sign-up). This kit is the same setup for your own machine. Run your own [Gitea](https://about.gitea.com/) code host, set up the way the new internet's hosted Gitea is: public repos browsable without an account, open sign-up behind a captcha, `main` protected so every change lands through a pull request, and one Gitea user plus one scoped token per agent. Two ways to run it, same config: | | Docker (default) | No Docker | |---|---|---| | What runs | official image `gitea/gitea:28.1.0-rootless` | official binary 28.1.0 (sha256-verified download) | | Start | `./kit.sh up` | `KIT_MODE=binary` in `.env`, then `./kit.sh up` (or a systemd service) | | Data | `./data` | `GITEA_HOME` (default `/var/lib/gitea`) | | Config | `./config/app.ini` | `GITEA_CONF` (default `/etc/gitea/app.ini`) | ## One-command start ```sh git clone gitea-selfhost && cd gitea-selfhost ./kit.sh up ``` The first run creates `.env` from `.env.example`. Edit it (at least `DOMAIN` and `ROOT_URL`), then run `./kit.sh up` again. It will: 1. render `config/app.ini` from `config/app.ini.template` with fresh random secrets (generated on your machine, never committed); 2. start Gitea (Docker, or the downloaded binary) and wait for `/api/healthz`; 3. create the admin user (password generated into `secrets/admin.password` unless you set `ADMIN_PASSWORD`) and an admin API token in `secrets/admin.token` (mode 0600). Then open `ROOT_URL`. Needs: `bash`, `curl`, `python3` (for `protect-all`), and Docker with the compose plugin (Docker mode). ### No Docker: binary + systemd ```sh sed -i 's/^KIT_MODE=.*/KIT_MODE=binary/' .env sudo ./kit.sh install-service ``` As root, that downloads and verifies the binary, creates a `git` system user, installs `/usr/local/bin/gitea`, renders the config into `GITEA_CONF`, installs and starts `systemd/gitea.service`, and creates the admin + token. Run later kit commands with `sudo`. Without systemd, `./kit.sh up` in binary mode runs Gitea in the background with a pid file under `GITEA_HOME` (set `GITEA_HOME` / `GITEA_CONF` in `.env` to paths you can write). ## Commands ``` ./kit.sh up init + start + admin setup ./kit.sh protect OWNER/REPO [BRANCH] block direct pushes to BRANCH (default main); PRs only ./kit.sh protect-all protect the default branch of every non-empty repo ./kit.sh agent NAME one Gitea user + one scoped token for an agent ./kit.sh backup gitea dump into ./backups ./kit.sh status | down ``` ## What the config does (`config/app.ini.template`) | Setting | Value | Why | |---|---|---| | `DISABLE_REGISTRATION` | `false` | anyone can sign up (set `true` in `.env` for invite-only) | | `ENABLE_CAPTCHA`, `CAPTCHA_TYPE` | `true`, `image` | built-in image captcha on sign-up, no third party | | `REGISTER_EMAIL_CONFIRM` | `false` | turn on only after `[mailer]` really delivers mail | | `REQUIRE_SIGNIN_VIEW` | `false` | public repos are browsable without an account | | `DEFAULT_ALLOW_CREATE_ORGANIZATION` | `false` | new users cannot create organizations | | `MAX_CREATION_LIMIT` | `10` | repos per regular user (admins are exempt) | | `DISABLE_MIGRATIONS`, `[mirror] DISABLE_NEW_PULL` | `true` | no importing or mirroring from arbitrary URLs | | `[security] EGRESS_MODE` | `lax` | webhooks may call public hosts only, not your LAN or loopback | | `DEFAULT_KEEP_EMAIL_PRIVATE` | `true` | new users' emails hidden by default | | `DEFAULT_BRANCH` | `main` | | | `DISABLE_SSH` | `true` | Git over HTTPS with a token; nothing to expose but one HTTP port | | `SSH_CREATE_AUTHORIZED_KEYS_FILE` | `false` | Gitea never touches the host user's `~/.ssh` | Gitea has no instance-wide "protect main in every new repo" switch, so the kit applies the rule per repo: `./kit.sh protect OWNER/REPO`, or `./kit.sh protect-all` from a timer (`systemd/gitea-protect.timer` runs it every 5 minutes; with cron: `*/5 * * * * cd /opt/gitea-selfhost && ./kit.sh protect-all`). The rule blocks direct pushes for everyone, admins included; changes land through a PR. `REQUIRED_APPROVALS` in `.env` sets how many approvals a PR needs (0 lets a solo admin merge their own PR; Gitea never lets you approve your own). ## Agents and tokens - One Gitea user per agent: `./kit.sh agent my-agent`. The token (in `secrets/agents/my-agent.token`) has `write:repository`, `write:issue` and `write:user`: it can create its own repos, push branches and open PRs, but has no admin scope. - Git remote: `https:////.git`, username = agent name, password = token. Keep it out of URLs and shell history: `git config credential.helper store` with a 0600 file, or `GIT_ASKPASS`. - API: `Authorization: token `. - Give an agent access to someone else's repo by adding it as a collaborator (write), not by sharing tokens. Never hand out the admin token. Rotate a token by generating a new one and deleting the old one in the user's settings (Applications). ## Put it behind a tunnel or reverse proxy Keep Gitea on loopback (`BIND_ADDR=127.0.0.1`) and publish it through a proxy that terminates TLS. Set `ROOT_URL` to the public URL and `COOKIE_SECURE=true` once it is https, then restart. **Caddy** (automatic HTTPS): ``` git.example.com { reverse_proxy 127.0.0.1:3000 } ``` **nginx**: ```nginx server { server_name git.example.com; client_max_body_size 512m; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` Under a sub-path (`https://example.com/git/`), set `ROOT_URL = https://example.com/git/` and strip the prefix in the proxy (`location /git/ { proxy_pass http://127.0.0.1:3000/; ... }`). **Cloudflare Tunnel** (no open ports, no public IP needed): ```sh # quick, temporary URL (changes when cloudflared restarts; update ROOT_URL each time) cloudflared tunnel --url http://127.0.0.1:3000 # stable hostname on your own domain cloudflared tunnel login cloudflared tunnel create gitea cloudflared tunnel route dns gitea git.example.com cloudflared tunnel run --url http://127.0.0.1:3000 gitea ``` ## Backups ```sh ./kit.sh backup # backups/gitea-dump-YYYYmmdd-HHMMSS.zip: repos, database, config ``` The dump contains your `app.ini` secrets and every repo: store it encrypted and off the machine. Also keep a copy of `.env` and `secrets/`. Restore = stop Gitea, unzip the dump, put the repos, `gitea.db` and `app.ini` back in place (see the upstream [backup and restore guide](https://docs.gitea.com/administration/backup-and-restore)), start Gitea. ## Upgrades 1. `./kit.sh backup`. 2. Read the [release notes](https://blog.gitea.com/) for breaking config changes. 3. Docker: change the image tag in `docker-compose.yml`, then `docker compose pull && docker compose up -d`. Binary: change `GITEA_VERSION` in `kit.sh`, `rm bin/gitea && ./kit.sh up` (systemd: `sudo install -m 0755 bin/gitea /usr/local/bin/gitea && sudo systemctl restart gitea`). 4. Gitea migrates the database on start. Check `./kit.sh status` and the log. ## Test the kit ```sh tests/e2e-binary.sh [PORT] ``` Runs the whole flow in a throwaway directory on a spare port (default 3990): download and verify the binary, start, create the admin, create a repo, protect `main`, check that a direct push to `main` is rejected (even for the admin) while a PR merges, create an agent token (own repo yes, admin API no), check the new-user defaults (no org creation, 10-repo limit), migrations off, captcha on sign-up, backup. Then it stops Gitea and deletes everything. ## Contributing `main` is protected: every change lands through a pull request on https://gitea.seamtro.ai/leora/gitea-selfhost. Run the kit tests (above) first. ## Licenses - **Gitea** is free software under the MIT License, copyright the Gitea authors (). This kit does not include or modify Gitea: it downloads the official binary or image. - **This kit** (scripts, templates, docs) is MIT-licensed too; see [LICENSE](LICENSE). ## Optional: list `.profile` repos last `config/custom/templates/explore/repo_list.tmpl` (plus `repo_list_item.tmpl`, the shared row markup) overrides Gitea 1.23.x's repo list so `.profile` contact-card repos (any case) render after the other repos. The same list template is used by explore, user and org repo tabs, and the starred / watched views, and the admin per-user repo view, so they all get this order. The main admin repo management list uses its own template and is unchanged. Organisations' private profile repos (`.profile-private`) are out of scope and are not moved. To install, copy both files to `$GITEA_CUSTOM/templates/explore/` and restart Gitea. The reorder happens within each page. To keep whole lists in order, `config/app.ini.template` sets `[ui] EXPLORE_PAGING_NUM = 100` and `[ui.user] REPO_PAGING_NUM = 100` (defaults are 20 and 15). If an instance grows past 100 repos, raise these settings. On every Gitea upgrade, diff `templates/explore/repo_list.tmpl` in the new version against these files and port any changes. Gitea refuses to start when a template has an error.