Skip to Content

How to Self-Host Gitea: The Developer's Guide to Private Git Hosting

Run your own Git server with Gitea, PostgreSQL, and Docker Compose — complete with SSH, HTTPS, and automated backups.

How to Self-Host Gitea: The Developer's Guide to Private Git Hosting

GitHub is great until you need full control over your repositories, want to avoid per-seat pricing, or just prefer keeping your code on infrastructure you own. Gitea is a lightweight, self-hosted Git service that gives you everything you need — issues, pull requests, CI/CD, wikis, and more — in a single Docker container. This guide walks you through exactly how to self-host Gitea with PostgreSQL, Docker Compose, SSH passthrough, and HTTPS.

For a deeper dive into the Docker + PostgreSQL architecture, see Own Your Code: How to Self-Host Gitea on Your Own Server with Docker and PostgreSQL. If you prefer Caddy as your reverse proxy, our Production Guide: Deploy Gitea with Docker Compose + Caddy + PostgreSQL on Ubuntu has you covered. For Traefik users, check out Production Guide: Deploy Gitea with Docker Compose + Traefik + PostgreSQL on Ubuntu.

What You'll Need Before You Start

Gitea is lightweight, but running it well in production means having the right foundation.

  • VPS or dedicated server with 2 CPU cores, 4GB RAM, and 20GB disk minimum
  • Ubuntu 22.04/24.04 or Debian 12 with Docker and Docker Compose v2 installed
  • A domain or subdomain pointed at your server (required for HTTPS)
  • Ports 22, 80, and 443 available (or custom alternatives if you know what you're doing)
  • Basic familiarity with Git, Docker, and reverse proxies

SQLite works for solo use, but this guide uses PostgreSQL — it's the right choice for teams, larger repositories, and anything you care about keeping stable.

1. Set Up Docker and Project Structure

Create a dedicated directory for Gitea. Keeping everything in one place makes backups and upgrades easier.

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

Create the Docker Compose file:

version: "3.8"

networks:
  gitea:
    external: false

services:
  server:
    image: docker.gitea.com/gitea:1.26.4
    container_name: gitea
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__database__DB_TYPE=postgres
      - GITEA__database__HOST=db:5432
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=gitea
      - GITEA__server__DOMAIN=git.yourdomain.com
      - GITEA__server__ROOT_URL=https://git.yourdomain.com/
      - GITEA__server__SSH_DOMAIN=git.yourdomain.com
      - GITEA__server__START_SSH_SERVER=true
      - GITEA__server__SSH_PORT=22
      - GITEA__server__SSH_LISTEN_PORT=22
      - GITEA__service__DISABLE_REGISTRATION=false
      - GITEA__service__REQUIRE_SIGNIN_VIEW=false
    restart: always
    networks:
      - gitea
    volumes:
      - ./gitea:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3000:3000"
      - "222:22"
    depends_on:
      - db

  db:
    image: docker.io/library/postgres:16-alpine
    container_name: gitea-db
    restart: always
    environment:
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=gitea
      - POSTGRES_DB=gitea
    networks:
      - gitea
    volumes:
      - ./postgres:/var/lib/postgresql/data

Start the stack:

docker compose up -d

Visit http://your-server-ip:3000 to complete the web installer. When prompted for database settings, use db as the host (the container name), port 5432, and the credentials from your Compose file.

Why pin the image version?

Always pin your Gitea tag. gitea/gitea:latest will eventually upgrade underneath you and break something. Pin to a specific version like 1.26.4, test upgrades in a staging environment, and bump deliberately.

2. Configure SSH Passthrough

Gitea runs its own SSH server inside the container, but you want users to connect to port 22 on your host. There are two approaches: let Gitea bind port 22 directly (simplest), or use SSH passthrough to delegate authentication to the host.

Option A: Direct port binding (recommended for most setups)

If Gitea is the only service using SSH on this server, bind port 22 directly to the container. In your Compose file, the port mapping "222:22" means external port 222 maps to internal port 22. Change it to "22:22" if you want standard SSH:

ports:
  - "3000:3000"
  - "22:22"

Then update Gitea's SSH settings in the web UI or via environment variables:

- GITEA__server__SSH_PORT=22
- GITEA__server__SSH_LISTEN_PORT=22

Option B: SSH passthrough (if you need host SSH too)

If your host needs SSH access for administration, run the host SSH daemon on a different port and let Gitea take port 22. Or use SSH passthrough by creating a user on the host that forwards to the container:

# On the host
sudo useradd -m -s /bin/bash git
sudo -u git mkdir -p /home/git/.ssh

# Create the authorized_keys command
echo 'command="ssh -p 222 git@localhost \"$SSH_ORIGINAL_COMMAND\"",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty' | \
  sudo tee /home/git/.ssh/authorized_keys

Then map user SSH keys into the container and update the Compose port to "222:22" so the passthrough works.

3. Add HTTPS with a Reverse Proxy

Never run Gitea over plain HTTP in production. Use Caddy, Nginx, or Traefik to terminate TLS.

Caddy (simplest option)

# Caddyfile
git.yourdomain.com {
  reverse_proxy localhost:3000
}

Run Caddy in Docker alongside Gitea:

services:
  caddy:
    image: caddy:2
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    networks:
      - gitea

volumes:
  caddy-data:
  caddy-config:

Nginx (if you already use it)

server {
  listen 443 ssl http2;
  server_name git.yourdomain.com;

  ssl_certificate /path/to/cert.pem;
  ssl_certificate_key /path/to/key.pem;

  location / {
    proxy_pass http://localhost: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;
  }
}

After setting up the reverse proxy, update GITEA__server__ROOT_URL to https://git.yourdomain.com/ and restart Gitea.

4. Secure Your Installation

Defaults are friendly. Production needs lockdown.

Disable open registration

Unless you want random people creating accounts, turn off self-registration after creating your admin account:

- GITEA__service__DISABLE_REGISTRATION=true
- GITEA__service__REQUIRE_SIGNIN_VIEW=true

Set strong secrets

Gitea auto-generates secrets on first run, but you should set them explicitly for reproducible deployments:

docker run -it --rm docker.gitea.com/gitea:1.26.4 gitea generate secret SECRET_KEY
docker run -it --rm docker.gitea.com/gitea:1.26.4 gitea generate secret INTERNAL_TOKEN

Add the output to your environment variables:

- GITEA__security__SECRET_KEY=your-generated-secret
- GITEA__security__INTERNAL_TOKEN=your-generated-token

Enable two-factor authentication

Require 2FA for all admin accounts. Go to Site Administration → Authentication Sources or enable it per-user in account settings. Gitea supports TOTP (Google Authenticator, Authy, etc.) and WebAuthn hardware keys.

5. Back Up and Restore

Your repositories live in /data/gitea-repositories. Your database lives in PostgreSQL. Back up both.

Automated backup script

#!/bin/bash
# /opt/gitea/backup.sh

BACKUP_DIR="/opt/gitea/backups"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR

# Backup database
docker compose exec -T db pg_dump -U gitea gitea > \
  $BACKUP_DIR/gitea_db_$DATE.sql

# Backup repositories and config
tar czf $BACKUP_DIR/gitea_data_$DATE.tar.gz -C /opt/gitea gitea

# Keep only last 7 backups
ls -t $BACKUP_DIR/gitea_db_*.sql | tail -n +8 | xargs -r rm
ls -t $BACKUP_DIR/gitea_data_*.tar.gz | tail -n +8 | xargs -r rm

echo "Backup completed: $DATE"

Add to cron for daily backups:

chmod +x /opt/gitea/backup.sh
(crontab -l 2>/dev/null; echo "0 2 * * * /opt/gitea/backup.sh >> /var/log/gitea-backup.log 2>&1") | crontab -

Restore from backup

# Stop Gitea
docker compose down

# Restore data
tar xzf /opt/gitea/backups/gitea_data_20260115_020000.tar.gz -C /opt/gitea

# Restore database
docker compose up -d db
sleep 5
docker compose exec -T db psql -U gitea -d gitea < \
  /opt/gitea/backups/gitea_db_20260115_020000.sql

# Start everything
docker compose up -d

6. Upgrade Gitea

Upgrades are straightforward if your data is persisted properly.

# 1. Backup first
/opt/gitea/backup.sh

# 2. Update the image tag in docker-compose.yml
# image: docker.gitea.com/gitea:1.26.4 -> 1.27.0

# 3. Pull and restart
docker compose pull
docker compose up -d

# 4. Check logs for migration output
docker compose logs -f server

Always read the release notes before upgrading. Major versions may require manual migration steps.

7. Tips, Gotchas, and Troubleshooting

Here is what breaks when you self-host Gitea — and how to fix it fast.

Permission denied on repositories

Gitea runs as UID 1000 by default. If you use host bind mounts, the directory owner must match:

sudo chown -R 1000:1000 /opt/gitea/gitea

SSH clone URLs show wrong port

If Gitea shows ssh://[email protected]:222/... but you want clean SCP-style URLs, set:

- GITEA__server__SSH_PORT=22
- GITEA__repository__USE_COMPAT_SSH_URI=false

Database connection fails on first start

The Gitea container starts faster than PostgreSQL on first run. The depends_on in Compose only waits for the container, not for the database to be ready. If you see connection errors, restart Gitea:

docker compose restart server

Large file uploads fail

Increase the attachment size limit in your environment:

- GITEA__repository__upload__FILE_MAX_SIZE=100
- GITEA__repository__upload__MAX_FILES=10

Emails not sending

Configure SMTP in your environment or app.ini:

- GITEA__mailer__ENABLED=true
- GITEA__mailer__PROTOCOL=smtps
- GITEA__mailer__SMTP_ADDR=smtp.gmail.com
- GITEA__mailer__SMTP_PORT=465
- [email protected]
- GITEA__mailer__PASSWD=your-app-password

Closing Thoughts

Self-hosting Gitea gives you complete ownership of your code, your issues, your CI/CD pipelines, and your data. With Docker Compose, PostgreSQL, and a solid backup strategy, you have a Git platform that rivals anything SaaS — without the recurring costs or third-party dependencies.

Start simple: Gitea + PostgreSQL + Caddy. Lock it down with HTTPS, disable open registration, and enable 2FA. Automate your backups. Then iterate — add Actions runners, package registries, or mirror external repositories.

If you're running this in production and need help with scaling, security hardening, or migration from GitHub/GitLab — get in touch with our team. We build and maintain self-hosted Git infrastructure for teams that value control.

OpenClaw Docker VPS Deploy: A Developer's Step-by-Step Guide
Get OpenClaw running on your own VPS with Docker Compose, persistent storage, and production-ready security — no local install needed.