Self-hosted Git server: Forgejo, Gitea or cgit
Four ways to run a self-hosted Git server, ranked by RAM: bare repos over SSH, cgit, Forgejo or Gitea, and GitLab. See what a 1 GB VPS can host.
Which self-hosted Git server should you run
A self-hosted Git server is not one product, and the RAM (random access memory) on your VPS decides which version of it you can have. Git needs no daemon of its own: a bare repository plus an SSH (secure shell) account is already a working server on the smallest box you can rent. Everything above that line is a web application you are choosing to run next to it, and each step up costs memory a small VPS may not have.
There are four steps. A bare repository over SSH, with nothing listening that was not listening already. cgit, a fast read-only web view with no database. Forgejo or Gitea, a full forge with accounts, issues and pull requests in a few hundred megabytes. GitLab, which expects a server many times the size of the others.
Decide by the work you need to do, then check the memory figure against the plan you are paying for.
How much RAM each option really needs
Only two of these projects publish a hardware figure. Treat a published figure as a floor rather than a promise, and measure your own instance once it runs, with systemd-cgtop or ps -o rss= -C forgejo.
The data behind this chart
[
{
"label": "Gitea, small team",
"ram_gb": 1
},
{
"label": "GitLab, memory constrained",
"ram_gb": 8
},
{
"label": "GitLab, single node baseline",
"ram_gb": 16
}
]Gitea documents 1 GB of RAM with 2 CPU cores as typically sufficient for small teams and projects, and names a Raspberry Pi 3 as enough for small workloads. GitLab documents 16 GB as the baseline for a single node installation, and 8 GB as the low end for what its own page calls a memory constrained environment. Forgejo publishes no hardware requirement at all. It is a fork of Gitea and behaves like one, so the Gitea figure is the closest published guide you have.
What that means on a 1 GB VPS: bare repositories and cgit fit with room left over, because neither runs a resident service. Forgejo or Gitea will start and will serve a small team on SQLite, but you are sitting on the documented floor, so leave PostgreSQL and the CI (continuous integration) runner off that box. If the web interface vanishes without an error, run sudo dmesg -T | grep -i oom and look for a line like Out of memory: Killed process 1181 (forgejo), which means the kernel out of memory killer took it. GitLab on a 1 GB box is not a tuning problem. It will not run.
Tier 0: a bare repository over SSH
Git has no network daemon you need to start. git push over SSH runs git-receive-pack on the far end as an ordinary Unix process, so any account you can reach with a key is already a Git remote. Make one account for the repositories, and keep the repositories outside its home directory, because on Ubuntu 24.04 a new home directory is mode 0750 and a web view added later cannot read into it.
sudo adduser --system --shell /bin/bash --gecos 'Git Version Control' \
--group --disabled-password --home /home/git git
sudo install -d -m 0755 -o git -g git /srv/git
sudo -u git git init --bare /srv/git/project.git--bare creates a repository with no working copy, which is what a server holds. Pushing into a repository that does have a working copy is refused with refusing to update checked out branch: refs/heads/main, and that is the most common mistake at this tier.
Now give the account a key and clone it.
sudo -u git install -d -m 700 /home/git/.ssh
sudo -u git tee -a /home/git/.ssh/authorized_keys <<'EOF'
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexamplekeyhere alice@laptop
EOF
sudo -u git chmod 600 /home/git/.ssh/authorized_keysgit remote add origin git@vps.example.com:/srv/git/project.git
git push -u origin mainA first push that worked ends with * [new branch] main -> main. One that ends with git@vps.example.com: Permission denied (publickey) was never authenticated, so read the server log with sudo journalctl -u ssh -n 20. A line reading Authentication refused: bad ownership or modes for file /home/git/.ssh/authorized_keys means the file mode is wrong, because sshd ignores a key file that other users can write.
Then take the shell away from the account.
command -v git-shell | sudo tee -a /etc/shells
sudo chsh -s "$(command -v git-shell)" gitgit-shell accepts only the few commands Git sends over SSH, so an interactive login now stops with a message instead of a prompt:
fatal: Interactive git shell is not enabled.
hint: ~/git-shell-commands should exist and have read and execute access.That is the entire server. There is no database, and no web process to upgrade. What you give up is everything a forge does: no browsing, no issue tracker, no pull requests, and no per-user permission. Every key in that file can read and write every repository the git user owns.
Tier 1: cgit gives you a web view without a database
cgit is a CGI (common gateway interface) program written in C. The web server runs it once per request, it reads the repositories straight off the disk, and it stores no state of its own. Ubuntu 24.04 carries it in the universe component.
sudo apt update
sudo apt install -y cgit fcgiwrap nginx
sudo install -d -o www-data -g www-data /var/cache/cgitPoint it at the repository directory in /etc/cgitrc:
root-title=Git on example.com
css=/cgit.css
logo=/cgit.png
cache-size=1000
cache-root=/var/cache/cgit
snapshots=tar.gz zip
scan-path=/srv/gitscan-path walks that directory and lists every repository it finds, so a new bare repo appears with no extra configuration. cache-size is the number of cached pages, and caching stays off while it is zero. Read what your package already put in /etc/cgitrc before you add lines, since the Debian and Ubuntu package ships some defaults of its own.
Each entry shows the first line of the repository's description file, so a fresh bare repo lists itself as Unnamed repository; edit this file 'description' to name the repository. Fix that once per repository:
echo 'Project X, internal tooling' | sudo -u git tee /srv/git/project.git/descriptionThe nginx site file, and how to check it
server {
listen 80;
server_name git.example.com;
root /usr/share/cgit;
try_files $uri @cgit;
location @cgit {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/cgit/cgit.cgi;
fastcgi_param PATH_INFO $uri;
fastcgi_param QUERY_STRING $args;
fastcgi_param HTTP_HOST $server_name;
fastcgi_pass unix:/run/fcgiwrap.socket;
}
}sudo systemctl enable --now fcgiwrap.socket
sudo nginx -t && sudo systemctl reload nginx
systemctl show fcgiwrap.socket -p Listenroot /usr/share/cgit serves cgit.css and cgit.png as plain files, and try_files hands everything else to the CGI at /usr/lib/cgit/cgit.cgi. A 502 page, with connect() to unix:/run/fcgiwrap.socket failed (2: No such file or directory) in /var/log/nginx/error.log, means the socket unit is not running or listens at another path. The systemctl show line prints the path it actually uses.
Two limits are worth knowing before you build on it. cgit is read-only and has no login, so everything under scan-path is public: keep a private repository out of that directory, or put HTTP basic authentication in front of the whole site. And the CGI runs as the web server user, so that user needs to traverse /srv/git and read each repository. A directory it cannot enter shows up as an empty index rather than an error.
Tier 2: Forgejo or Gitea for issues and pull requests
Forgejo and Gitea are the same idea: one Go binary that serves a web forge with users, organisations, issues, pull requests, releases, a package registry and a built-in CI system. Binary plus SQLite is the whole install, which is why they fit on hardware GitLab will not touch. The Compose file below is the one in the Forgejo documentation, with the image tag it names as of August 2026.
networks:
forgejo:
external: false
services:
server:
image: codeberg.org/forgejo/forgejo:16
container_name: forgejo
environment:
- USER_UID=1000
- USER_GID=1000
restart: always
networks:
- forgejo
volumes:
- ./forgejo:/data
- /etc/localtime:/etc/localtime:ro
ports:
- '3000:3000'
- '222:22'docker compose up -d
docker compose ps
curl -sI http://127.0.0.1:3000 | head -1The curl line should print an HTTP status line. Before you finish the first-run setup it may be a redirect to /install, which still means the service is up. If the container exits instead, the usual cause is ownership: the ./forgejo directory has to belong to the UID (user id) in USER_UID, or the process cannot write its own data directory. Docker Compose on a VPS covers that file layout and the volume ownership rule in full.
Two answers on the setup page decide whether clone URLs work. The SSH port must be 222, because the Compose file maps host port 222 to the container's port 22, and the domain must be the name people will actually type. Get either wrong and every repository page offers a clone command that fails for everyone who copies it. Both live in the [server] section of app.ini afterwards, as SSH_PORT, SSH_DOMAIN and ROOT_URL.
For a public instance, publish the web port on the loopback address only ('127.0.0.1:3000:3000') and put nginx in front of it for TLS (transport layer security). Gitea installs the same way from the gitea/gitea image, or as a single binary with one systemd unit and one app.ini, and its current stable release is 1.27.1 as of August 2026.
Stay on SQLite while you can. It keeps the instance to one process and one file, and it survives a reboot with no extra service to supervise. PostgreSQL earns its cost when several people write at once, because SQLite serialises writes and long CI runs write constantly. Both projects can move an existing instance to PostgreSQL later, so this is not a decision you are stuck with.
Forgejo or Gitea: what actually differs
The lineage is shared. Gitea forked from Gogs in 2016. In late 2022 control of the Gitea domain and trademark passed to a company, Gitea Ltd, and several maintainers together with Codeberg started Forgejo. Forgejo is published by Codeberg e.V., a non-profit association registered in Germany, and it moved from the MIT licence to GPLv3 (GNU general public license version 3) in 2024. Gitea stays MIT licensed and is developed with commercial backing behind it.
Day to day the feature sets are close. The path between them is not. Forgejo v10.0, from January 2025, was the last release that could take a Gitea database directly, and only from Gitea v1.22 or older. Gitea is on 1.27.1 as of August 2026, so a current Gitea instance has no supported in-place switch to Forgejo. Pick one before you fill it with data, and treat any later move as an export and a re-import.
A short rule for choosing. If governance matters to you, or you want the project to stay with a non-profit, run Forgejo. If you want the larger install base and a commercial support option, run Gitea. Both are maintained in the open and release often: Forgejo ships a stable release every three months and an LTS (long term support) release each year, with v16.0.2 current and v15.0.6 the LTS as of August 2026.
Tier 3: what GitLab costs before it does anything
GitLab CE is a different class of software. One instance is a set of cooperating services: Puma for the web application, Sidekiq for background jobs, PostgreSQL, Redis, Gitaly for repository access, and nginx in front. The Omnibus package installs them together, which makes the install simple and the memory floor high.
GitLab's requirements page documents 16 GB of RAM and 8 vCPU as the baseline for a single node installation, with 8 GB named as the low end in a memory constrained environment. The same page tells you to disable swap, because swapping under load degrades the instance badly. Those are the published figures as of August 2026, and they have climbed over the years, so read the page again before you size a server.
You get real things for that budget: a container registry, a package registry, fine-grained permissions, compliance and audit features, and CI that has been tested at large scale. If nobody on your team can name something from that list they need this quarter, you are paying for a bigger VPS in exchange for nothing.
The SSH access model: one git user and many keys
Every tier here authenticates the same way. There is one Unix account named git, and every public key goes into that account's ~/.ssh/authorized_keys. Authentication is the key. Authorisation is whatever options you write in front of the key on the same line.
A plain key line hands the holder whatever that account can do. A forced command narrows it to Git:
restrict,command="git-shell -c \"$SSH_ORIGINAL_COMMAND\"" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexamplekeyhere alice@laptoprestrict, available since OpenSSH 7.2, turns off port forwarding, agent forwarding, X11 and PTY (pseudo terminal) allocation in one word. command= replaces whatever the client asked for with the one you name, and Git still works because Git sends its request in $SSH_ORIGINAL_COMMAND.
A forge writes that file for you, and that is the real difference between tier 0 and tier 2. Forgejo and Gitea rewrite authorized_keys with one line per registered key, each carrying a forced command that names the key by its database id:
command="/usr/local/bin/forgejo --config=/etc/forgejo/app.ini serv key-3",no-port-forwarding,no-x11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexamplekeyhere aliceThat forced command is how one shared Unix account becomes per-user permission: key-3 tells the forge which user is connecting, and it checks that user against the repository before any objects move. Do not hand-edit that file on a forge-managed box, because it is rewritten from the database and your line disappears. Deploy keys come from the same mechanism: a deploy key is an ordinary SSH key registered against a single repository, usually read-only, with the check done in the forge instead of in sshd.
Two habits matter more than any of the configuration above. Issue one key per person or per machine, never a shared key, because revoking a shared key means rotating it for everybody at once. And remove keys the day someone leaves, since an old key in that file is a permanent login nobody is watching. Good SSH key management on a server covers key types and passphrases, and all of it applies here unchanged. If the box is new, the first ten minutes on a new VPS is the right thing to do before you put repositories on it.
Can I run GitHub Actions on my own Git server?
You can run workflows written in the GitHub Actions syntax. You cannot run GitHub. Forgejo Actions has been enabled by default since Forgejo v1.21 and reads workflow files from .forgejo/workflows in each repository. Gitea Actions works the same way and reads .gitea/workflows. Both need a second program, the runner, installed and registered against your instance with a token from the admin settings. Many published actions run unchanged; anything that calls the GitHub API or expects GitHub-hosted infrastructure does not.
Plan for two consequences. The runner starts a container for every job, so it needs a container engine and a memory budget of its own, which is why it does not belong on the same 1 GB box as the forge. And the runner executes whatever a workflow file says, which Forgejo's documentation states in plain words: the runner performs remote code execution. Give it its own host where you can, or at minimum its own unprivileged user and a registration token scoped to one repository.
If your repositories are staying on GitHub and you only want the compute on hardware you control, that is a different setup with different steps: a self-hosted GitHub Actions runner attaches to a GitHub repository and needs none of this. If you are still weighing what leaving costs you, what GitHub actually gives you separates the Git hosting from the network around it.
Backups: repositories are only half the state
A bare repository is a directory, so copying it copies everything in it. A mirror clone from another machine is a real backup, and it refreshes in place:
git clone --mirror git@vps.example.com:/srv/git/project.git
cd project.git && git remote updateThat pulls every ref and every object. It does not pull server-side hooks or the description file, so keep a file-level copy of the directory too if you use hooks.
A forge keeps issues, pull requests, users, keys and permissions in its database, and a copy of the repositories alone throws all of that away. Both projects ship a dump command that writes the database, the repositories, the configuration and the attachments into one archive:
sudo -u git forgejo dump -c /etc/forgejo/app.ini -f /var/backups/forgejo-dump.zipUnder Docker the same command runs inside the container, and the configuration path depends on the image, so look before you type:
docker compose exec server ls /data/gitea/conf
docker compose exec -u git server forgejo dump -c /data/gitea/conf/app.iniRun it as the user that owns the data, and write the archive to a directory that user can write. Then copy the archive off the server, because a backup that only exists on the machine being backed up is not a backup. Restoring is the step people skip: unpack one dump onto a spare box now, so you learn the procedure at a calm moment rather than during an outage.
Pick by scenario
One person with a laptop and a VPS, no browsing needed: bare repositories over SSH. There is no extra service running and nothing to upgrade.
The same, plus you want to read code in a browser and send links to it: add cgit. Still no database, still nothing resident.
A team that reviews each other's code and tracks issues: Forgejo or Gitea, on 2 GB of RAM or more. Move the CI runner to a second box once jobs get real.
An organisation that needs a container registry and audit trails, with 16 GB to spend on the server: GitLab. Below that budget, do not start it.
Moving up the first three tiers is cheap, because in all of them the repositories are ordinary Git directories on disk. Start at the lowest tier that does the job. If you are working out what else deserves space on the same server, the shortlist of what is worth self-hosting puts a Git server next to the other services competing for that RAM.
FAQ
Can a 1 GB VPS run Forgejo or Gitea?
Yes, for a small team, on SQLite, with nothing else heavy on the box. Gitea's documentation gives 1 GB of RAM and 2 CPU cores as typically sufficient for small teams and projects, and Forgejo is a fork of Gitea with the same shape. Do not add PostgreSQL or a CI runner to that machine. If the service disappears with no error in its own log, run sudo dmesg -T | grep -i oom: a line naming the killed process means the kernel out of memory killer took it, and the answer is a bigger plan rather than a tuning flag.
What is the difference between Forgejo and Gitea?
They share a codebase history and most features. Gitea forked from Gogs in 2016, and Forgejo forked from Gitea in late 2022 after control of the Gitea trademark moved to a company. Forgejo is published by Codeberg e.V., a non-profit in Germany, under GPLv3; Gitea stays MIT licensed with commercial backing. The practical difference is the migration path. Forgejo v10.0, from January 2025, was the last release that could take a Gitea database directly, and only from Gitea v1.22 or older, so a current Gitea instance has no supported in-place switch.
Can I run GitHub Actions workflows on a self-hosted Git server?
Forgejo Actions and Gitea Actions both run workflows written in GitHub Actions YAML syntax, read from .forgejo/workflows and .gitea/workflows. You install a separate runner program and register it against your instance. Many published actions work unchanged, while anything calling the GitHub API does not. The runner executes arbitrary code from your repositories and starts a container per job, so give it its own host, or at least its own unprivileged user, and keep it off a 1 GB server that is already running the forge.
How do I back up a self-hosted Git server?
For bare repositories, git clone --mirror from another machine copies every ref and object, and git remote update inside that mirror refreshes it. For Forgejo or Gitea, the repositories are only part of the state, because issues, pull requests, users and keys live in the database. Use the built-in dump, sudo -u git forgejo dump -c /etc/forgejo/app.ini, or the same command inside the container for a Docker install. Copy the archive off the server, and restore one onto a spare machine once so you know the procedure works.