Running a Self-Hosted Git Server (Gitea) on a VPS: CPU, RAM, and Disk Baseline

Gitea is often described as lightweight, and compared to GitLab it is, but lightweight is not free. A self-hosted Git server still runs a database, an SSH daemon, a web front end, an indexer, and background jobs for mirroring and garbage collection. The numbers below come from a production Gitea instance serving twenty developers over eight months, with peaks captured during merge-heavy days.

The baseline matters for two decisions: what plan to rent, and how much headroom to keep before the server starts evicting page cache during a clone storm. Our VPS plan breakdown maps the sizing below against available tiers.

Component Footprint at Rest

A stock Gitea binary with SQLite runs in about 90 MB of RSS on Debian 12 with no repositories. The web process forks for each request, and under load each fork briefly doubles the memory while handling a clone. Add the SSH daemon, a small reverse proxy, and systemd overhead, and the floor is roughly 400 MB before any repository data.

With Postgres instead of SQLite, add 150–250 MB for the database’s shared buffers and work memory. That combination idles at 600–700 MB and is the practical floor for a team instance. Anything below 2 GB of total RAM starts to hurt when simultaneous clones arrive.

Measuring Load During a Clone Storm

Clones are the dominant CPU event. Each clone runs a pack negotiation and compression step, which is CPU-bound. A repository with a 500 MB history will spike one core for 15–40 seconds on a cold cache. Track this with `pidstat -u -p $(pgrep gitea) 1` during a burst and record peak CPU-seconds per clone.

Ten developers cloning the same large repository within a minute creates a queue. Gitea’s built-in cache helps only for identical packs; different branches still compress independently. On a two-vCPU instance, that burst pushes load average above 4 and pushes clone times past a minute. Gitea 1.21+ has a repository-level cache setting that reduces repeated pack work; enable it before blaming the hardware.

Memory Under Concurrent Fetches

Memory during fetch is bounded by the pack size, not the repository size. A `git fetch` that pulls 50 MB of new objects allocates roughly that much plus overhead in the web process. Twenty concurrent fetches of that size need about 1.5 GB just for the transient allocations. This is the number that catches teams off guard after a big merge.

Cap this with `[server] LFS_START_SERVER` settings if you use LFS, and monitor `git count-objects -vH` to see loose object growth. Loose objects make fetches slower and larger; a weekly `git gc –auto` on active repositories keeps the pack size stable and the memory profile predictable.

Disk Growth Projections

Disk usage comes from three places: bare repositories, attachments and issue data, and the search index. Compute the first by summing `du -sh` over each repository directory after a `gc`. The rule of thumb is that the bare repo is 0.8–1.2 times the working copy size for text-heavy code and much larger for binary assets.

Team size Repos Bare repo storage Attachments Index Total
5 developers 20 4 GB 500 MB 200 MB 12 GB
20 developers 80 25 GB 2 GB 800 MB 40 GB
50 developers 200 80 GB 8 GB 2.5 GB 120 GB

Database Choice Changes the Baseline

SQLite is fine up to about ten concurrent users and then write locking becomes visible in the UI as stalled issue creation. Postgres removes the lock contention but adds memory and maintenance: vacuum must run, connections consume RAM, and the WAL directory grows under heavy activity. Check `pg_stat_activity` during the busy hour before assuming the database is not the bottleneck.

For a twenty-person team, SQLite with `PRAGMA journal_mode=WAL` and a busy timeout works and keeps the footprint smaller. The moment a CI system starts polling the API at high rates, the write lock becomes the limiting factor and the migration to Postgres is worth the extra 200 MB.

Background Jobs and Their Cost

Gitea runs scheduled tasks for mirror syncing, repository health checks, and garbage collection. A mirrored repository with a large remote costs a full fetch every interval, and a dozen mirrors can consume a steady 5–10% of one core. Set `[mirror] DEFAULT_INTERVAL` to a sane value rather than the default and stagger the jobs.

The repository indexer is the other background consumer. Bleve indexing runs after commits and spikes memory to roughly the index size. On a 2 GB instance it can push the system into swap, which is why the index belongs on a separate volume when it exceeds 2 GB. Monitor with `journalctl -u gitea -f` for indexing errors before they compound.

Recommended Baseline

For a team of up to twenty with active repositories, the measured baseline is 4 GB RAM, 2 vCPU, and 80 GB SSD. That plan leaves roughly 1.5 GB for page cache and absorbs a ten-clone burst without swapping. Teams with CI runners co-located on the same host should double the RAM and add two vCPUs.

Beyond thirty developers, move the database to a separate instance and keep Gitea on a modest 4 GB node. This isolates write contention and lets each component be scaled on its own curve. Size for the peak you measured, add 30% headroom, and revisit after the first quarter. The main site lists plans that match these thresholds, so you can compare without overcommitting on a slow Wednesday.

Leave a Reply