# Gitea Self-hosted Gitea behind nginx, backed by MySQL. ```text internet -> (TLS terminated upstream) -> nginx :80 -> gitea :3000 -> mysql :3306 ``` Three services on two networks: `frontend` (nginx + gitea) and `backend` (gitea + db, `internal`, so the database has no route out). ## Setup ```bash cp .env.example .env openssl rand -base64 32 # put the result in MYSQL_PASSWORD ./setup.sh ``` `setup.sh` creates the bind mounts, fixes their ownership, and brings the stack up. It refuses to start if `MYSQL_PASSWORD` is still the placeholder. The web installer is disabled (`GITEA_INSTALL_LOCK=true`) because everything it would ask is already set in `.env`. Create the first admin from the CLI: ```bash docker compose exec -u git gitea gitea admin user create \ --username admin --email you@example.com --random-password --admin ``` Registration is closed by default (`GITEA_DISABLE_REGISTRATION=true`); flip it in `.env` if you want open signups. ## TLS The stack terminates nothing: nginx listens on plain `:80` and expects TLS to be handled by whatever sits in front (Cloudflare, a host-level proxy, a load balancer). That upstream must send `X-Forwarded-Proto: https`, otherwise Gitea builds redirects with the wrong scheme. `GITEA_PROTOCOL=http` with an `https://` `GITEA_ROOT_URL` is correct for this layout. If you also want the real client IP in Gitea's logs, uncomment the `set_real_ip_from` block in [nginx/gitea.conf](nginx/gitea.conf) and point it at the upstream proxy's network. ## SSH Git over SSH runs on the host port in `GITEA_SSH_PORT` (2222), mapped to port 22 in the container, where the image's own OpenSSH daemon serves it. `GITEA_SSH_PORT` only tells Gitea which port to advertise in clone URLs. Leave `GITEA_START_SSH_SERVER=false`. Setting it to `true` makes Gitea's built-in SSH server try to bind port 22, which sshd already holds, and the container exits with `bind: address already in use`. ## Backup ```bash # Database docker compose exec db mysqldump \ -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" --single-transaction "$MYSQL_DATABASE" \ | gzip > gitea-db-$(date +%F).sql.gz # Repositories, avatars, LFS tar czf gitea-data-$(date +%F).tar.gz data/ ``` Restore the database with `docker compose exec -T db mysql -u... -p... gitea < dump.sql`, then restore `data/` and start the stack. ## Upgrading `GITEA_IMAGE` is pinned to a minor series (`gitea/gitea:1.26`) so `docker compose pull` picks up patch releases but never jumps a major version, which would migrate the database irreversibly. Back up before changing the series, and read the release notes for that jump.