Self-Hosting·7 min read·

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.

NB

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.

Gitea request flow git push / web client 443 Caddy TLS reverse proxy 127.0.0.1 Gitea webapp+git SQL Postgres db service ./data volume repos+settings

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.

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

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

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

bash
git remote add mirror https://git.example.com/user/repo.git
git push --mirror mirror

Takeaway

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