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.

🐾 What you'll need:
  • 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.

GiteaForgejo
OriginOriginal project (2016)Fork (2022)
GovernanceGitea Ltd (company-backed)Codeberg e.V. (community-backed nonprofit)
Feature parityMarginally aheadTracks Gitea closely, with own additions
Notable usersMany self-hosters, enterpriseCodeberg.org (100k+ users)
Migration between themTrivial (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:

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 → SettingsSSH / GPG KeysAdd 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:

  1. Create a GitHub personal access token with repo scope (read access is sufficient)
  2. In Gitea: + → New Migration → GitHub
  3. Paste the repo URL and your GitHub token
  4. Pick which items to migrate (wiki, issues, PRs, labels, releases)
  5. 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:

  1. In app.ini (inside the container at /data/gitea/conf/app.ini), enable Actions
  2. Add a .gitea/workflows/build.yml file in your repo with standard Actions syntax
  3. Register a runner — pull the gitea/act_runner image, 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

PlanUsersReposNotes
Starter ($7.99, 1 GB)~25 active devsUnlimitedSmall teams, side projects
Pro ($15.99, 4 GB)~150 active devsUnlimitedMid-size teams, with CI runners
Premium ($35.99, 8 GB)~500 active devsUnlimitedLarger 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.

🐱
OliveVPS Team

We migrated 47 private repos from GitHub to a self-hosted Gitea on a Pro plan over a weekend. The migration script handled everything except a few wiki pages with weird markdown. Total monthly cost: $15.99 instead of GitHub Teams' $40 for the same team.