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.