gitea-selfhost/README.md
2026-10-15 20:23:37 +08:00

8.5 KiB

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 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

git clone <this repo> 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

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://<your host>/<owner>/<repo>.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 <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:

server {
    server_name git.example.com;
    client_max_body_size 512m;
    location / {
        proxy_pass /gitea;
        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 /gitea/; ... }).

Cloudflare Tunnel (no open ports, no public IP needed):

# quick, temporary URL (changes when cloudflared restarts; update ROOT_URL each time)
cloudflared tunnel --url /gitea
# 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 /gitea gitea

Backups

./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), start Gitea.

Upgrades

  1. ./kit.sh backup.
  2. Read the release notes 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

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 (https://github.com/go-gitea/gitea). 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.

Optional: list .profile repos last

config/custom/templates/explore/repo_list.tmpl overrides Gitea 1.23.x's repo list so that .profile contact-card repos render after the other repos on each page (explore, user and org pages). Copy it to $GITEA_CUSTOM/templates/explore/ and restart Gitea. The reorder is per page, so it is exact while all repos fit on one page ([ui] EXPLORE_PAGING_NUM, default 20).