Self-Hosting a Personal Git Server with Gitea
Run Gitea on a VPS with Docker Compose to build a self-contained GitHub-style forge with issue tracking, Actions CI, and total control over your repositories.
Netbay Engineering
Netbay Engineering
On this page
Relying on a public forge for every private repo is convenient, but it puts your source, your branches, and your git hooks in someone else's hands. A personal Git server gives you full control, no monthly repository limits, and a clean path to automate build pipelines against code you actually own. Gitea is the lightest serious option: a single Go binary that bundles repository management, issues, pull requests, actions, and an elegant web UI.
For developers who keep personal projects, dotfiles, configs, and experiments around, a private forge removes the friction of paying for private repositories or agonizing over which scraps of code can stay public. Because Gitea speaks plain HTTP and SSHs like any git remote, your existing workflow barely changes: the remote URL points at your domain instead of a third-party host, and everything else behaves the same.
This walkthrough runs Gitea in Docker on a fresh Ubuntu 24.04 VPS. We cover a clean Compose setup, reverse-proxy notes with Caddy, and the security settings that matter before you push anything sensitive.
Why Gitea Over Overkill Alternatives
Gitea competes with GitLab and the lighter Gogs. For a personal box the trade-offs are clear. Memory is the most visible one: GitLab drags in a suite of background watchers and services that want gigabytes of RAM, while Gitea idles happily with a few hundred megabytes. That difference matters on a small VPS you also want to run other services on.
Beyond memory, Gitea keeps the deployment story trivial. It is one database plus one container, not a constellation of interdependent jobs. When you upgrade, you pull a new image and restart; there is no migration dance across a dozen services to coordinate.
The feature set is genuinely complete for solo work:
- Embedded UI with repositories, issues, milestones, pull requests, and releases.
- Git LFS built in, so large binaries do not bloat your repo objects.
- A built-in Actions runner for CI without extra infrastructure.
- Second-factor authentication, webhooks, and organization support.
If you outgrow it, the data model is plain SQL, so migrating elsewhere later is manageable.
The Compose Stack
The cleanest route is two containers glued together with Compose: the Gitea server and a Postgres database.
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__server__ROOT_URL=https://git.example.com
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=db:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=change-this-db-password
volumes:
- ./data:/data
- /etc/timezone:/etc/timezone:ro
ports:
- "127.0.0.1:3000:3000"
restart: unless-stopped
depends_on:
- db
db:
image: postgres:16-alpine
container_name: gitea-db
environment:
- POSTGRES_USER=gitea
- POSTGRES_PASSWORD=change-this-db-password
- POSTGRES_DB=gitea
volumes:
- ./db:/var/lib/postgresql/data
restart: unless-stoppedNotable choices deserve explanation. Postgres is used instead of SQLite because concurrent Actions runners will hammer the file lock and serialization SQLite relies on. The app is bound to 127.0.0.1 so nothing is exposed to the network before the reverse proxy decides to let traffic through. And the database lives on a named volume so future upgrades do not wipe history.
The environment variables map directly onto Gitea's config keys. The double-underscore notation tells Gitea to interpret each variable as a nested config path, so GITEA__database__PASSWD becomes the database password setting. Point ROOT_URL at your public domain so generated git URLs always match the web URL.
Routing Traffic With Caddy
Exposing Gitea to the internet means terminating TLS somewhere. Caddy is the lowest-friction proxy because it obtains and renews Let's Encrypt certificates automatically, with no external tooling and no manual renewals.
git.example.com {
reverse_proxy 127.0.0.1:3000
encode gzip
}Because the gitea container only listens on localhost, traffic must arrive via the host proxy. Set the app's ROOT_URL to the same public hostname so git URLs and web URLs match. Once you restart Caddy, opening the domain runs Gitea's first-run installer, which completes the initial admin and repo configuration.
Hardening Checklist
Before you make it public, keep these in mind. First, generate a random, long database password and move it out of the Compose file into an environment file or a secret manager, so credentials are not committed anywhere. Second, create a dedicated non-root user for the container and run everything as that user rather than root.
The security settings you configure once save you constant trouble later:
- Enable two-factor authentication under Site Administration.
- Set DISABLE_REGISTRATION to true so strangers cannot create accounts.
- Restrict the Actions runner to trusted repositories.
- Back up the data and db volumes on a schedule, and test the restore.
You can mirror an existing repo in as little as one command:
git remote add mirror https://git.example.com/user/repo.git
git push --mirror mirrorTakeaway
Gitea turns a single VPS into a private collaboration hub with real issue tracking and CI, all under your control and free of per-user pricing. The single-binary design keeps operations cheap, and the Postgres backend keeps it reliable as your repositories grow.
The whole stack fits comfortably on a 2 GB Ubuntu VPS. You can spin up an Ubuntu 24.04 instance on Netbay in under 60 seconds and follow along — netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance