Back to blog
Blog

Self-Host Forgejo on a VPS With Docker Compose

Self-host Forgejo on a VPS with Docker Compose, HTTPS, backups, and safe updates—an operator-focused guide for teams leaving GitHub/GitLab.

By Anurag Singh
Updated on Oct 20, 2026
Category: Blog
Share article
Self-Host Forgejo on a VPS With Docker Compose

You don’t need an “enterprise” platform to run repos, issues, and pull requests. If you want predictable costs and control over your data, self-host Forgejo on a VPS. Keep it boring (in a good way): one app, one database, disciplined backups, and upgrades you schedule.

This post frames Forgejo as a practical alternative to GitHub/GitLab. It then walks through a VPS deployment with Docker Compose, an Nginx reverse proxy with Let’s Encrypt, restorable backups, and update habits that won’t ruin your Monday.

Forgejo in one sentence (and why teams pick it)

Forgejo is a self-hostable Git forge for repos, pull requests, issues, releases, webhooks, and runner-based automation. It targets teams that want a familiar “GitHub workflow” while keeping source code and metadata under their own control.

Licensing matters in 2026, especially if you standardize tooling across clients. Forgejo is open source under the MIT license.

It grew from the Gitea codebase and puts community governance front and center.

Who should self-host Forgejo on a VPS (and who shouldn’t)

Forgejo is a good fit if you:

  • Run small-to-mid-sized teams that mainly need Git + PRs + issues.
  • Want SSO later (OIDC/SAML via an identity provider) without buying into a heavy platform now.
  • Prefer predictable performance and storage on a VPS or dedicated server you control.
  • Need a private forge for client work, internal tooling, or regulated environments.

You’ll be happier elsewhere if “Git hosting” means “a built-in CI/CD product, container registry, artifact management, and a long menu of extras.” Forgejo integrates with that ecosystem, but it doesn’t try to be the ecosystem.

A quick comparison: Forgejo vs GitHub/GitLab (hosted) vs Gitea

This is usually the decision tree:

  • GitHub (hosted): quick to start and a huge ecosystem, but your repos and metadata live in a third-party account model and costs can climb with seats and add-ons.
  • GitLab (hosted or self-managed): powerful and feature-rich, but heavier to run. Self-managing GitLab is often a “dedicated ops budget” choice.
  • Forgejo (self-hosted): lighter, simpler to operate, and well-suited to straightforward infrastructure.
  • Gitea (self-hosted): also lightweight and widely used. Forgejo’s main difference is its governance direction and project goals.

The practical takeaway: if you want a GitHub-like forge with sane resource use, Forgejo sits in the sweet spot. If you want an all-in-one delivery platform, plan on GitLab or plan on integrating external CI/build tooling.

Infrastructure planning: VPS sizing, storage, and DNS

Git forges are storage-first. CPU is rarely the limiting factor. Disk I/O, capacity, and backup throughput matter more.

Size around repo growth, LFS usage, and retention.

  • Small team (1–10 users, modest repos): 2 vCPU, 4 GB RAM, NVMe storage. Start with 80–160 GB disk.
  • Growing team (10–50 users, frequent CI pulls, lots of LFS): 4 vCPU, 8 GB RAM, 200–500 GB NVMe.
  • Large repos / heavy LFS usage: prioritize disk capacity and IOPS. Consider a HostMyCode dedicated server if you need sustained I/O under load.

Use a dedicated subdomain such as git.example.com. Point an A/AAAA record at your server.

If you’re migrating from an existing forge, lower DNS TTL ahead of cutover.

This walkthrough pairs well with this DNS cutover tutorial.

If you want a straightforward starting point without overbuying, a HostMyCode VPS gives you fixed CPU/RAM plus full root access for Docker, firewall rules, and backup automation.

Deployment overview (what we’re actually going to run)

A sensible “operator default” stack looks like this:

  • Forgejo: the app (web UI + Git over SSH/HTTP).
  • PostgreSQL: the database backend (SQLite is fine for tiny installs, but Postgres is easier to grow and restore cleanly).
  • Nginx: reverse proxy and TLS termination with Let’s Encrypt.

Docker Compose keeps deploys and upgrades repeatable. That’s the real goal: fewer one-off fixes and fewer surprises.

Install Docker on Ubuntu/Debian (the minimum you should standardize)

The examples assume Ubuntu 24.04 LTS or Debian 12/13 on a clean VPS. Run them as a sudo-capable user.

sudo apt update
sudo apt install -y ca-certificates curl gnupg

# Docker Engine
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo 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" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

sudo usermod -aG docker $USER

Log out/in once so your user can run docker without sudo.

Before you expose anything, put a baseline firewall in place. If you want a quick UFW setup, see Set Up UFW Firewall on Ubuntu for a Web Server.

Docker Compose for Forgejo + PostgreSQL

Create a working directory and persistent volumes. /opt is a clean place to keep app stacks:

sudo mkdir -p /opt/forgejo
sudo chown -R $USER:$USER /opt/forgejo
cd /opt/forgejo

Create docker-compose.yml.

The image tags below use latest on purpose because Forgejo’s upstream tags change over time. In production, pin to a specific version once you’ve tested upgrades.

services:
  db:
    image: postgres:16
    container_name: forgejo-db
    environment:
      POSTGRES_DB: forgejo
      POSTGRES_USER: forgejo
      POSTGRES_PASSWORD: change-me-long-random
    volumes:
      - ./postgres:/var/lib/postgresql/data
    restart: unless-stopped

  forgejo:
    image: codeberg.org/forgejo/forgejo:latest
    container_name: forgejo
    depends_on:
      - db
    environment:
      USER_UID: 1000
      USER_GID: 1000
      FORGEJO__database__DB_TYPE: postgres
      FORGEJO__database__HOST: db:5432
      FORGEJO__database__NAME: forgejo
      FORGEJO__database__USER: forgejo
      FORGEJO__database__PASSWD: change-me-long-random
      FORGEJO__server__ROOT_URL: https://git.example.com/
      FORGEJO__server__DOMAIN: git.example.com
      FORGEJO__server__HTTP_PORT: 3000
      FORGEJO__server__SSH_DOMAIN: git.example.com
      FORGEJO__server__SSH_PORT: 2222
    volumes:
      - ./data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "127.0.0.1:3000:3000"
      - "2222:2222"
    restart: unless-stopped

Notes that prevent the usual misconfigurations:

  • HTTP is bound to localhost (127.0.0.1:3000) so only Nginx can reach it.
  • SSH is exposed on port 2222. That keeps it separate from your server SSH on port 22 and makes firewall rules easier to reason about.
  • ROOT_URL must match HTTPS or you’ll end up with incorrect clone URLs and finicky OAuth callbacks.

Bring it up:

docker compose up -d
docker compose ps
docker logs -n 50 forgejo

At this point Forgejo is reachable on the VPS at http://127.0.0.1:3000. Next, put it behind Nginx and enable TLS.

Nginx reverse proxy + HTTPS (clean, predictable, debuggable)

Install Nginx and Certbot:

sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx

Create an Nginx server block at /etc/nginx/sites-available/forgejo:

server {
    listen 80;
    listen [::]:80;
    server_name git.example.com;

    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;

        proxy_read_timeout 300;
        proxy_connect_timeout 60;
        client_max_body_size 200m;
    }
}

Enable it and reload:

sudo ln -s /etc/nginx/sites-available/forgejo /etc/nginx/sites-enabled/forgejo
sudo nginx -t
sudo systemctl reload nginx

Issue a Let’s Encrypt certificate:

sudo certbot --nginx -d git.example.com

If you want a tighter TLS checklist (renewals, common breakages, and debugging), keep this TLS setup guide nearby.

It covers the problems you actually run into: incorrect DNS, HTTP-01 routing issues, stale AAAA records, and renewal conflicts.

SSH access for Git: what to open, what to document

Forgejo will listen for Git SSH on port 2222 (as defined in Compose). Open that port only to the networks that need it.

“Anywhere” works, but you can usually be stricter:

  • Allow TCP 2222 from office IPs or your VPN only.
  • Keep server admin SSH locked down separately (keys only, no password login).

If your admins already use a VPN, this pairs well with Set Up WireGuard VPN on Ubuntu VPS for Admin Access.

From a developer laptop, the clone URL will look like:

git clone ssh://git@git.example.com:2222/yourorg/yourrepo.git

Put that URL format in your onboarding docs. It eliminates a lot of “why doesn’t SSH work?” messages.

Backups: treat Forgejo like a database app (because it is)

Most self-hosted outages turn into backup stories. For Forgejo, a usable backup has three parts:

  • Database dump (PostgreSQL): users, issues, PRs, and all the metadata.
  • Forgejo data directory: repos, attachments, LFS objects, avatars, config.
  • Encryption + offsite copy: “on the same VPS” is just extra disk usage.

Here’s a practical backup script that writes timestamped files under /var/backups/forgejo.

Create /usr/local/sbin/forgejo-backup.sh:

#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="/var/backups/forgejo"
TS="$(date -u +%Y%m%dT%H%M%SZ)"
STACK_DIR="/opt/forgejo"

mkdir -p "$BACKUP_DIR"

# 1) Database dump
cd "$STACK_DIR"
docker exec -t forgejo-db pg_dump -U forgejo -d forgejo | gzip > "$BACKUP_DIR/db-$TS.sql.gz"

# 2) App data (repos, LFS, attachments, config in /data)
tar -C "$STACK_DIR" -czf "$BACKUP_DIR/data-$TS.tar.gz" data

# 3) Optional: keep permissions readable only by root
chmod 600 "$BACKUP_DIR"/*-$TS.*

echo "Backup complete: $TS"

Make it executable:

sudo chmod +x /usr/local/sbin/forgejo-backup.sh

Schedule it with cron or systemd timers. If you prefer logged schedules with fewer cron footguns, use the approach in this systemd timer guide.

Offsite is the real requirement. Push backups to a second location (object storage, another VPS, or a backup server) using encrypted sync.

If you want a runbook approach that includes a tested restore plan, the format in this disaster recovery tutorial fits Git forges well.

Updates: how to upgrade without gambling your workday

Compose upgrades stay boring if you stick to two rules:

  • Back up first. Every time. Automate it, then still confirm the backup exists before you touch production.
  • Move in controlled steps. Don’t skip months of releases without reading upstream breaking changes.

A typical upgrade sequence looks like this:

cd /opt/forgejo
sudo /usr/local/sbin/forgejo-backup.sh

docker compose pull

docker compose up -d

docker logs -n 50 forgejo

If you pinned a specific image tag (recommended after your first stable month), change it intentionally. Commit the Compose file to a private ops repo. Upgrade during a maintenance window.

One detail operators regularly miss: database major upgrades. PostgreSQL major bumps require a migration plan.

Don’t accidentally jump versions because an image tag moved. If you’d rather not deal with OS patching and baseline maintenance at all, this is where managed VPS hosting can be worth it.

Hardening checklist for a forge that holds real IP

A Git forge isn’t “just another web app.” It often contains credentials in history, deployment manifests, private security issues, client code, and audit trails.

Lock down the basics early:

  • Firewall: only expose 80/443 and Forgejo SSH port (2222). Keep Docker ports bound to localhost where possible.
  • Admin accounts: enforce 2FA for owners/maintainers; disable unused admin users.
  • Mail settings: if you enable notifications, configure SMTP properly and align DNS records (SPF/DKIM/DMARC) for the sending domain.
  • Limits: set reasonable max upload sizes in Nginx and Forgejo to prevent accidental giant pushes through the web UI.
  • Rate limiting: if you see brute-force behavior, apply targeted Nginx limits on login endpoints.

If you need dependable notification email (password resets, mentions), use Set Up DKIM, SPF, And DMARC For A Domain as your DNS baseline.

And if you run your own relay on the VPS, this Postfix relay guide lays out the operator-grade path.

Backups are only real if you can restore: a 10-minute restore drill

A restore drill doesn’t need to turn into a quarterly initiative. Do a small test early, while your instance is still small and the stakes are low.

  1. Provision a temporary VPS (or a second small VM) and install Docker + Compose.
  2. Bring up the stack once to create the directories, then stop it.
  3. Restore data-*.tar.gz into /opt/forgejo/data.
  4. Restore db-*.sql.gz into Postgres (drop/recreate the DB if needed).
  5. Start Forgejo and verify you can log in, browse repos, and view issues.

If you haven’t done restore testing on a VPS before, the approach in VPS Backup Verification (2026) carries over cleanly: automate the boring validation checks, and keep a readable runbook for everything else.

Operational tips you’ll care about after week two

Once the setup stops moving, a few small decisions keep it that way:

  • Separate “platform” and “product” repos: keep Docker, Nginx, and backup scripts in an internal ops repo.
  • Decide on LFS early: if you’ll store binaries, enable Git LFS intentionally and document quotas.
  • Watch disk growth: repos only grow. Add monitoring or at least alerts for free space.
  • Plan for migrations: if you’re importing from GitHub/GitLab, test imports on a staging Forgejo first.

If you already have monitoring on your servers, reuse the alerting pattern in this monitoring setup guide to catch disk pressure and CPU spikes before users feel it.

Summary: a lightweight forge you can run like infrastructure

Forgejo hits a useful middle ground in 2026: familiar Git workflows without the operational weight of a full platform. Keep HTTP behind a reverse proxy, isolate SSH, and treat backups/restores as part of the service.

It runs well on a VPS.

If you’re leaving a hosted provider, treat the move like a real migration. Plan DNS, validate email, rehearse restores, and upgrade on purpose. That’s how “self-hosted” stays professional instead of painful.

For predictable performance and room to grow, start with a HostMyCode VPS and expand storage as your repos grow. If you want help with patching and the underlying server upkeep, managed VPS hosting keeps the operational load low.

If you want Forgejo to behave like infrastructure, not a weekend experiment, run it on a VPS with predictable resources and snapshots you control. Start with a HostMyCode VPS for full root access, or choose managed VPS hosting if you’d rather hand off OS patching and baseline hardening.

FAQ

Should I use SQLite or PostgreSQL for Forgejo?

SQLite works for very small installs, but PostgreSQL is the safer default for team usage. Backups, restores, and growth are simpler with a dedicated database service.

Do I need a dedicated server to self-host Forgejo?

Not usually. Most teams run Forgejo comfortably on a 2–4 vCPU VPS with NVMe storage. Consider a dedicated server if you have very large repos, heavy LFS usage, or strict I/O requirements.

Can I put Forgejo behind Cloudflare or another CDN?

Yes for HTTPS and caching static assets, but keep Git SSH on a direct port and make sure your reverse proxy passes the right headers. For strict IP allowlists, terminate at Nginx and control ingress there.

What’s the safest way to update Forgejo with Docker Compose?

Back up first, pull new images, then restart the stack. Pin Forgejo to a tested image tag once you’re stable, and avoid accidental PostgreSQL major upgrades without a migration plan.

What should I back up for a Forgejo restore?

At minimum: a PostgreSQL dump and the Forgejo /data directory (repos, LFS, attachments, config). Store an encrypted copy offsite and run a restore drill early.