GitHub is great until Microsoft decides to train Copilot on your private repos, your country gets sanctioned and access is cut, or you simply don't want a tech giant owning your source code. Gitea and Forgejo are the answer — featherweight self-hosted Git servers with pull requests, issues, CI integration, and a container registry, all running comfortably on a $7.99 VPS. Same UX as GitHub at a fraction of the resource footprint.
This guide installs Gitea (the original) via Docker, with notes throughout for the Forgejo fork (governance-focused community fork that runs Codeberg.org). It walks through Postgres setup as the production backend, SSH access for git push/pull, HTTPS via Nginx + Let's Encrypt, GitHub Actions-compatible CI via Gitea Actions, and the operational pieces — backups, migrations from GitHub, OAuth integration for SSO.
- A Linux VPS — Ubuntu 22.04 or Debian 12, 1 GB RAM minimum
- A domain pointed at your VPS
- Roughly 30 minutes
For Gitea pre-installed and ready in 60 seconds, see our Git Hosting VPS plans — same hardware, same Gitea or Forgejo on request.
1. Gitea or Forgejo — which to pick
Quick context: Gitea is the original; Forgejo is a community-led fork that emerged in 2022 after some governance changes at Gitea Ltd. Both share most of the codebase, both work fine, both run the same UI you'd expect.
| Gitea | Forgejo | |
|---|---|---|
| Origin | Original project (2016) | Fork (2022) |
| Governance | Gitea Ltd (company-backed) | Codeberg e.V. (community-backed nonprofit) |
| Feature parity | Marginally ahead | Tracks Gitea closely, with own additions |
| Notable users | Many self-hosters, enterprise | Codeberg.org (100k+ users) |
| Migration between them | Trivial (same DB schema) | Trivial |
For most users, the choice doesn't matter — pick Gitea if you want the larger ecosystem, Forgejo if community governance matters to you. This guide uses Gitea for the install steps; the Forgejo install is identical except for the Docker image name (codeberg.org/forgejo/forgejo instead of gitea/gitea).
2. Prepare the VPS
apt update && apt upgrade -y
apt install -y curl ca-certificates gnupg ufw
ufw allow 22/tcp # SSH (we'll use port 2222 for Gitea SSH so this stays for system SSH)
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 2222/tcp # Gitea SSH
ufw --force enable
adduser git
usermod -aG sudo git
rsync --archive --chown=git:git ~/.ssh /home/git
3. Install Docker
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
usermod -aG docker git
4. Set up PostgreSQL
Gitea ships with SQLite support but Postgres is the production-grade choice. Both Gitea and Postgres run in containers from the same docker-compose file.
Switch to the git user and prep the project:
su - git
mkdir -p ~/gitea/{data,db}
cd ~/gitea
# Generate a strong DB password
DB_PASSWORD=$(openssl rand -hex 16)
echo "DB_PASSWORD=$DB_PASSWORD" > ~/.gitea-secrets
chmod 600 ~/.gitea-secrets
echo "$DB_PASSWORD"
Save the password somewhere safe.
5. Deploy Gitea / Forgejo
cat > ~/gitea/compose.yaml <<'EOF'
services:
gitea:
image: gitea/gitea:1.22 # For Forgejo: codeberg.org/forgejo/forgejo:8
container_name: gitea
restart: unless-stopped
depends_on:
- postgres
environment:
USER_UID: 1000
USER_GID: 1000
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: postgres:5432
GITEA__database__NAME: gitea
GITEA__database__USER: gitea
GITEA__database__PASSWD: PASTE_DB_PASSWORD_HERE
GITEA__server__DOMAIN: git.example.com
GITEA__server__SSH_DOMAIN: git.example.com
GITEA__server__ROOT_URL: https://git.example.com/
GITEA__server__SSH_PORT: 2222
GITEA__server__START_SSH_SERVER: "true"
ports:
- "127.0.0.1:3000:3000" # web (behind Nginx)
- "2222:22" # SSH for git operations
volumes:
- ./data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
postgres:
image: postgres:16-alpine
container_name: gitea-db
restart: unless-stopped
environment:
POSTGRES_USER: gitea
POSTGRES_PASSWORD: PASTE_DB_PASSWORD_HERE
POSTGRES_DB: gitea
volumes:
- ./db:/var/lib/postgresql/data
EOF
Replace both PASTE_DB_PASSWORD_HERE placeholders with the password you generated. Then launch:
docker compose up -d
docker compose logs -f
First boot takes 20–30 seconds. When you see Listen: http://0.0.0.0:3000, the app is up.
6. Nginx reverse proxy + HTTPS
sudo apt install -y nginx certbot python3-certbot-nginx
sudo tee /etc/nginx/sites-available/gitea <<'EOF'
server {
listen 80;
server_name git.example.com;
client_max_body_size 512M; # Large pushes need this
proxy_read_timeout 600s;
proxy_send_timeout 600s;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d git.example.com
7. First-run setup
Visit https://git.example.com — Gitea's install wizard appears. Most fields are pre-filled correctly from the env vars in compose.yaml. Confirm:
- Database settings — already correct from env vars
- Server settings — Domain, SSH port, base URL
- Admin account — set this now, scrolling to the bottom of the form. The first admin can only be set during install. Don't skip.
Click Install Gitea. The setup runs migrations (~10 seconds) and presents the login page. Sign in as your admin user.
8. SSH access for git push/pull
Gitea uses port 2222 for SSH (we mapped it that way to avoid colliding with system SSH on 22). Tell git locally:
# On your laptop, edit ~/.ssh/config
cat >> ~/.ssh/config <<'EOF'
Host git.example.com
Hostname git.example.com
Port 2222
User git
EOF
Add your SSH public key to your Gitea account: top-right avatar → Settings → SSH / GPG Keys → Add Key. Paste the contents of ~/.ssh/id_ed25519.pub.
Now you can clone and push as expected:
# Create a repo in the Gitea UI first, then:
git clone git@git.example.com:yourname/yourrepo.git
cd yourrepo
echo "Hello" > README.md
git add . && git commit -m "First commit"
git push origin main
🐾 Pre-installed Git hosting
Our Git Hosting VPS plans ship with Gitea (or Forgejo on request) pre-installed, Postgres configured, and SSL provisioned. From $7.99/mo, ready in minutes.
See Git Hosting Plans →9. Migrating from GitHub
Gitea has a built-in GitHub importer that handles repos, issues, pull requests, releases, and labels. To import a repo:
- Create a GitHub personal access token with
reposcope (read access is sufficient) - In Gitea: + → New Migration → GitHub
- Paste the repo URL and your GitHub token
- Pick which items to migrate (wiki, issues, PRs, labels, releases)
- Click Migrate Repository
For bulk migration of many repos, use the Gitea API or the gitea-mirror community tool. Migration speed: about 5–15 seconds per small repo, longer for repos with hundreds of issues or large LFS data.
10. Reference: CI, registry, integrations
Gitea Actions (GitHub Actions compatible)
Gitea ships built-in CI that's GitHub Actions-compatible at the YAML level. Most actions from actions/checkout, actions/setup-node, actions/setup-python work as-is. Configuration:
- In
app.ini(inside the container at/data/gitea/conf/app.ini), enable Actions - Add a
.gitea/workflows/build.ymlfile in your repo with standard Actions syntax - Register a runner — pull the
gitea/act_runnerimage, register with a token from your repo settings
For more substantial CI workloads, pair with our Woodpecker CI VPS — covered in our Woodpecker tutorial.
Container registry
Built-in Docker registry. Push images directly to Gitea:
docker login git.example.com
docker tag myimage git.example.com/yourname/myimage:latest
docker push git.example.com/yourname/myimage:latest
The registry is enabled by default. Images live alongside your repos in the database/storage and respect repo-level permissions.
OAuth / SSO
Gitea supports OAuth2 (GitHub, Google, GitLab, Discord, generic OIDC) and SAML. Configure under Site Administration → Authentication Sources → Add Source. Lets you offload user management to your existing identity provider.
Backups
Built-in backup command:
docker compose exec gitea gitea dump -c /data/gitea/conf/app.ini
# The dump lives in the gitea data volume; copy it out and push offsite
docker compose cp gitea:/app/gitea/gitea-dump-*.zip ./backups/
Pair with rclone to push backups to S3-compatible storage. Schedule daily via cron.
Per-plan capacity
| Plan | Users | Repos | Notes |
|---|---|---|---|
| Starter ($7.99, 1 GB) | ~25 active devs | Unlimited | Small teams, side projects |
| Pro ($15.99, 4 GB) | ~150 active devs | Unlimited | Mid-size teams, with CI runners |
| Premium ($35.99, 8 GB) | ~500 active devs | Unlimited | Larger orgs, LFS-heavy repos |
Git LFS
Gitea has Git LFS built in. Large binary files (game assets, ML models, video) live in the LFS store on the VPS. Enabled by default — just use git lfs track "*.psd" in your repo and push as normal. LFS storage counts against your VPS disk.
FAQ
Gitea vs Forgejo — really which?
Functionally identical for 99% of users. The choice comes down to governance preference: Gitea is company-backed (Gitea Ltd), Forgejo is community-backed (Codeberg e.V., a nonprofit). The codebases diverge slowly but stay compatible. Migrating between them is a database compatibility-level operation. Default to Gitea for the larger ecosystem; pick Forgejo if community governance matters to you.
Why use port 2222 for SSH instead of 22?
Because port 22 is already used by system SSH (you log into the VPS that way). Running both on port 22 means either Gitea handles SSH and you lose system login, or system SSH wins and Gitea SSH doesn't work. Port 2222 sidesteps the conflict. Some setups bind Gitea to port 22 and move system SSH to a non-standard port; either pattern works but 2222 for Gitea is the most common choice.
Can I host Gitea on the same VPS as my CI?
Yes, on a Pro or Premium plan. Gitea uses ~200 MB RAM idle; Woodpecker CI uses ~100 MB plus whatever your build needs at peak. Both fit comfortably in 4 GB+ together. For larger teams or CPU-heavy builds, separate them onto two VPS — Gitea on a small box, CI on a high-CPU box.
Does Gitea support Git LFS?
Yes — built in. Large binary assets (game art, video, ML models) work via standard git lfs track commands. LFS storage counts against your VPS disk, so plan for it on storage-heavy projects.
Can I use Gitea Actions to replace GitHub Actions entirely?
Mostly. The YAML syntax is GitHub Actions-compatible, and most setup actions (checkout, setup-node, setup-python) work as-is. Actions that require GitHub-specific APIs (deployments, environment protection rules, GitHub Pages) won't work. For straightforward build/test/deploy workflows, Gitea Actions is a drop-in replacement. For complex workflows depending on GitHub features, you might prefer pairing Gitea with Woodpecker CI for full control.
How do I make Gitea bigger than one VPS?
Three layers can be horizontally scaled: the web app (run multiple Gitea containers behind a load balancer, sharing the database and storage volume), the database (Postgres replicas), and the storage backend (S3-compatible object storage for repos via the MinIO adapter). Most self-hosters never need this — a single Premium VPS handles a 500-developer org comfortably. Beyond that you're probably better off with managed Git hosting.