How to Set Up a Private Git Server With Gitea the Right Way
Set up a self hosted Git server with Gitea: Docker Compose, SQLite or Postgres, SSH access, a reverse proxy, backups and first admin settings.

Hosting your own Git service gives you control over private code and costs one small container. Gitea is lightweight, with repositories, pull requests, issues and a built-in CI option, using a few hundred megabytes of memory. The risk is treating it as set and forget: a public Git server needs updates, backups and sensible access rules. It builds on Compose in production, and the setup assumes Gitea 1.22 on Docker 27.
Compose file
services:
gitea:
image: gitea/gitea:1.22
restart: unless-stopped
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__server__DOMAIN: git.example.test
GITEA__server__ROOT_URL: https://git.example.test/
GITEA__server__SSH_DOMAIN: git.example.test
GITEA__server__SSH_PORT: "2222"
GITEA__service__DISABLE_REGISTRATION: "true"
GITEA__service__REQUIRE_SIGNIN_VIEW: "true"
volumes:
- ./data:/data
ports:
- "127.0.0.1:3000:3000"
- "2222:22"Settings use the GITEA__section__KEY pattern to override app.ini. HTTP binds to localhost only, for the reverse proxy; SSH publishes on host port 2222 so it does not clash with the host's own sshd. SQLite is the default database and fits a handful of users; for a larger team add a Postgres service, as in the Compose layout of the previous post. Set the data directory ownership to the UID: sudo chown -R 1000:1000 data.
Reverse proxy and TLS
git.example.test {
reverse_proxy 127.0.0.1:3000
}
That is a complete Caddyfile; Caddy obtains a certificate automatically, as described in TLS with certbot or Caddy. The ROOT_URL must match the public address, or clone URLs and redirects point at the wrong host. For large pushes, raise any request size limit in the proxy.
First login and settings
docker compose up -d
docker compose exec -u git gitea gitea admin user create --admin \
--username admin --email [email protected] --password 'change-me-now'
docker compose logs -f --tail 20 giteaCreating the admin user from the command line avoids exposing an open install page. Change the password at first sign-in. Then review: disable registration (done above), require two-factor authentication for admins, turn off features you do not use and set limits on repository creation. Add your SSH public key in the profile and test with ssh -T -p 2222 [email protected]. Use an ~/.ssh/config entry so clone URLs work without the port, as in the SSH config guide.
Use it
git remote add origin ssh://[email protected]:2222/team/app.git
git push -u origin mainRepositories, issues and pull requests work as on other hosts. Migrate existing repositories with the built-in importer from GitHub or GitLab. See remotes and push for the workflow.
Back up and update
docker compose exec -u git gitea gitea dump -c /data/gitea/conf/app.ini -f /tmp/gitea-dump.zip
docker compose cp gitea:/tmp/gitea-dump.zip ./backups/gitea-$(date +%F).zip
sed -i 's#gitea/gitea:1.22#gitea/gitea:1.23#' compose.yaml
docker compose pull && docker compose up -dThe dump contains repositories, the database and configuration. Copy it off the server and test a restore on a spare machine. Read the release notes before changing the minor version, take a dump first and update one minor version at a time. Keep the host patched and consider restricting access by VPN or IP allowlist for a private instance.
Deciding whether to host it at all
A self-hosted Git service trades money for attention. The container is small, and the work is in everything around it: applying updates when security releases appear, watching disk usage as repositories grow, testing restores, and answering the person who cannot push at eight in the morning. For a team of three with a handful of private repositories, a hosted plan is cheaper once the time is counted. Self-hosting makes sense when code cannot leave your network, when you need to attach the service to an internal identity provider, or when the hosted limits on storage, runners or private collaborators cost more than a small server.
Be honest about who is on call. A Git server that only one person understands becomes a risk the day that person is away. Write down where the data lives, how to restore it and how to update it, and have a second person do an update from the notes once. If that exercise goes badly, the notes need work, or the server should be somebody else's product.
Data, storage and growth
All state lives in the data volume: the repositories as bare Git directories, attachments, the database and the configuration. That makes the layout easy to back up and easy to outgrow. Repositories with large binary files, release assets and package registries enlarge the volume faster than source code does. Set quotas for repository size and for attachments in the settings, and run the built-in repository maintenance, which garbage-collects and repacks. Watch the volume with a monitoring check that alerts at 80 percent used, since a full disk corrupts a database in the worst way: partly, and silently, until the next restart.
Keep the database choice in mind from the start. SQLite is simple and perfectly adequate for a small team with modest traffic, and a migration to PostgreSQL later is possible but not trivial. If the instance will serve CI runners that poll constantly, or dozens of users, begin with PostgreSQL and a separate volume, and back the database up with its own dump tool as well as through the application's archive.
Authentication and access
Disable open registration, require sign-in to view anything, and decide who may create organisations. For a team already using single sign-on, connect the service through OIDC or LDAP so that removing a person from the company directory removes their access, which a local account list never does reliably. Require two-factor authentication for administrators and enforce it for everyone if the policy allows. Review SSH keys and access tokens periodically: tokens created for an integration years ago, with write access to every repository, are the usual quiet exposure.
Plan the SSH side carefully. The container's SSH server shares the host's network, so a clone URL either needs the port in it or an agreed alias in each developer's SSH configuration. Many teams run the service's SSH on the standard port by giving it a dedicated IP address or by moving the host's own administrative SSH to another port. Choose one and document it, because every new teammate meets this question in their first hour.
Updates and restores in practice
Treat updates as routine. Subscribe to the release feed, read the notes for breaking changes, take a dump, pull the new image and start it. Gitea applies database migrations at startup, which is convenient and not reversible without the dump, so the dump is the rollback plan. Test the rollback once on a spare machine, because a backup that has never been restored is a hope. Time how long the restore takes and write that figure in the runbook; during an outage it is the number people ask for.
Where this does not apply
If you only need private repositories for a few projects, a hosted service costs less than the time to maintain a server. A team that depends on the server needs monitoring, tested backups and someone on call for it. Forgejo is a community fork with the same setup. For lighter needs, a bare repository over SSH works with no web interface, using the commands from deploy keys. See the Gitea Docker installation guide and the backup and restore guide.


