Journal indexDockup / field note
Note / self-host-gitea

How to Self-Host Gitea in 2026: Repositories, SSH and Upgrade Safety

Deploy Gitea with the right port, durable storage, TLS, authentication and backups. Troubleshoot when ROOT_URL generates localhost clone links in production.

If you already tried to self-host Gitea, the frustrating state is probably familiar: the UI appears, but ROOT_URL generates localhost clone links or the SSH port is not forwarded. Recreating the container rarely fixes a disagreement between URLs, state and dependencies.

This walkthrough uses one concrete completion criterion — clone over HTTPS and SSH, push a commit and LFS object, open an issue and run one job on a separately registered Actions runner. Every configuration choice is evaluated against that criterion rather than against a green container badge.

Find every durable byte in Gitea

List the state before the first real record is created: repositories, LFS objects, attachments, configuration and database. Mount /data before bootstrap, write harmless sample data and replace the container to prove that path is actually persistent. Confirm the mount by writing harmless data, replacing Gitea and reading it back.

Snapshots are valuable for quick rollback, but an independent backup is needed when the host or volume disappears. Restore into an empty environment with the pinned image and verify that repositories pass fsck, LFS objects download and issues, releases and user permissions match the pre-backup state. Use persistent volumes and snapshots to keep those two recovery mechanisms distinct.

Build a replaceable Gitea container

The following command makes the container boundary visible without pretending to provision every external service.

docker run -d \
  --name gitea \
  --restart unless-stopped \
  -p 127.0.0.1:3000:3000 \
  -v gitea-data:/data \
  -e GITEA__security__SECRET_KEY=replace-with-a-long-random-value \
  gitea/gitea:latest

Before opening ingress, inspect the resolved environment, mounts and listener. Add the reviewed connection settings for Postgres or MySQL for a busier installation and an SSH route if needed; use private names for private services. A successful launch ends when you can clone over HTTPS and SSH, push a commit and LFS object, open an issue and run one job on a separately registered Actions runner, not when docker ps prints Up.

Separate Gitea from its dependencies

Process health and product health are separate for Gitea. Port 3000 may answer while the user-facing transaction still fails. The network contract for Gitea is Postgres or MySQL for a busier installation and an SSH route if needed. Keep private endpoints on internal DNS, permit only required outbound calls and give Gitea a scoped service credential.

Use this readiness exercise after meaningful configuration changes: clone over HTTPS and SSH, push a commit and LFS object, open an issue and run one job on a separately registered Actions runner. Keep expensive external checks out of liveness probes so a provider outage does not cause a restart loop. Capacity work should track repository count, Git object packing, LFS storage, database latency and runner workload rather than ordinary page requests, which is closer to Gitea's real pressure than page requests.

TLS is easy; generated URLs are not

Expose one HTTPS hostname for Gitea; keep raw port 3000 private. Set ROOT_URL and SSH_DOMAIN to the addresses users actually clone. This prevents browsers and API clients from learning two competing addresses.

From a clean client, run the known-good transaction and inspect the first failing request. Use the custom-domain guide when DNS or TLS is wrong. Treat “ROOT_URL generates localhost clone links or the SSH port is not forwarded” as a separate application diagnosis once the route is proven.

Prove the Gitea deployment end to end

Do not make first-user traffic the acceptance test for Gitea. Prepare harmless sample state and run the complete action “clone over HTTPS and SSH, push a commit and LFS object, open an issue and run one job on a separately registered Actions runner”. Note the exact public URL, result, image reference and log interval associated with the run.

Replace the container and repeat without rebuilding data. Next, recover onto an empty host; the recovery condition is that repositories pass fsck, LFS objects download and issues, releases and user permissions match the pre-backup state. Observe repository count, Git object packing, LFS storage, database latency and runner workload rather than ordinary page requests in every pass and define an alert around degradation of the transaction rather than around idle container metrics.

One final check should fail on purpose: temporarily deny the test identity access to Postgres or MySQL for a busier installation and an SSH route if needed. Verify that the resulting Gitea message identifies the relevant boundary instead of triggering data deletion or an endless restart. Restore the valid condition and confirm the same sample transaction succeeds. Keep this short drill in the release checklist.

Rehearse the risky Gitea change

For Gitea, monitor a transaction rather than a process: clone over HTTPS and SSH, push a commit and LFS object, open an issue and run one job on a separately registered Actions runner. Combine its latency and error rate with repository count, Git object packing, LFS storage, database latency and runner workload rather than ordinary page requests so an alert identifies the constrained component.

The upgrade rehearsal must cover that schema migrations, repository hooks, packages and third-party runners need a staged Gitea upgrade. Restore, migrate and run the transaction before production replacement. If ROOT_URL generates localhost clone links or the SSH port is not forwarded, do not erase data to make startup green; compare version, variables, mounts and dependency reachability in that order.

Protect the valuable part of Gitea

After first login, review what an anonymous visitor, ordinary user and administrator can each do. The Gitea failure to avoid is keeping the installer or first admin account reachable longer than necessary. The intended policy is to close the installer after bootstrap, restrict site administration and keep runner registration tokens short-lived.

Treat GITEA__security__SECRET_KEY according to its Gitea role: keep sensitive values out of Git, document rotation effects and never substitute a public example in production. Keep dependency accounts separate from human accounts, deny unused egress where practical and cap work influenced by repository count, Git object packing, LFS storage, database latency and runner workload rather than ordinary page requests.

What Dockup should automate for Gitea

For Gitea, Dockup can create the route and TLS certificate, preserve mounts, deliver secrets and place Postgres or MySQL for a busier installation and an SSH route if needed on private networking while deploying to either Dockup or attached servers.

The release gate is still the concrete Gitea transaction: clone over HTTPS and SSH, push a commit and LFS object, open an issue and run one job on a separately registered Actions runner. Also verify the restore condition — repositories pass fsck, LFS objects download and issues, releases and user permissions match the pre-backup state. Those two checks show whether the deployment works and whether it can be recovered.

Frequently asked questions

What does Gitea need for a production deployment?

Route the Gitea container on port 3000 through one HTTPS origin. The supporting network requirement is Postgres or MySQL for a busier installation and an SSH route if needed. Do not call Gitea ready until you can clone over HTTPS and SSH, push a commit and LFS object, open an issue and run one job on a separately registered Actions runner.

Which Gitea data belongs in a backup?

Persist /data and include repositories, LFS objects, attachments, configuration and database in the same recovery manifest. A clean Gitea restore passes only when repositories pass fsck, LFS objects download and issues, releases and user permissions match the pre-backup state.

Does Gitea require HTTPS behind a reverse proxy?

Use HTTPS for the public Gitea origin and keep port 3000 on the internal route. Apply the Gitea setting correctly: set ROOT_URL and SSH_DOMAIN to the addresses users actually clone. For Gitea, HTTPS protects credentials or user content in transit and keeps origin-sensitive client behavior consistent.

How should a Gitea upgrade be tested?

Restore current Gitea state into an isolated deployment, apply the candidate version and repeat its acceptance transaction. Pay particular attention because schema migrations, repository hooks, packages and third-party runners need a staged Gitea upgrade. Keep the previous Gitea image until its data-migration and rollback boundary are understood.