Hosting Gitea & Forgejo with Docker, Nginx, and Cloudflare Proxy

This guide helps beginners deploy Gitea or Forgejo Git servers using Docker containers, secure them with Nginx reverse proxy, and protect with Cloudflare.

Replace all example domains like git.yourdomain.com with your real domain or subdomain.


Step 1: Project Directory and File Structure

Connect to your VPS via SSH and create a main project directory:

ssh your_user@your_vps_ip
mkdir -p ~/git-server/{gitea-data,forgejo-data,certs}
cd ~/git-server

Structure explanation:

  • gitea-data/ or forgejo-data/: persistent volumes where application and database data are stored, surviving container restarts.

  • certs/: stores SSL certificates from Certbot.

  • The main directory holds your Docker Compose and Nginx config files.

This clear separation helps with backups and easy upgrades.

Step 2: Docker Compose Files (Separate for Each Service)

Gitea Docker Compose (docker-compose-gitea.yml)

version: "3"

services:
  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__server__DOMAIN=git.yourdomain.com
      - GITEA__server__ROOT_URL=https://git.yourdomain.com/
      - GITEA__server__REVERSE_PROXY_TRUSTED_PROXIES=127.0.0.1 # Add Cloudflare IP ranges
    volumes:
      - ./gitea-data:/data
    networks:
      - git-net
    restart: always

  nginx:
    image: nginx:latest
    container_name: nginx-gitea
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx-gitea.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - gitea
    networks:
      - git-net
    restart: always

networks:
  git-net:
    driver: bridge

Forgejo Docker Compose (docker-compose-forgejo.yml)

version: "3"

services:
  forgejo:
    image: codeberg.org/forgejo/forgejo:latest
    container_name: forgejo
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - FORGEJO_APP__server__ROOT_URL=https://git.yourdomain.com/
      - FORGEJO_APP__server__TRUSTED_PROXIES=127.0.0.1 # Add Cloudflare IP ranges
    volumes:
      - ./forgejo-data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    networks:
      - git-net
    restart: always

  nginx:
    image: nginx:latest
    container_name: nginx-forgejo
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx-forgejo.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - forgejo
    networks:
      - git-net
    restart: always

networks:
  git-net:
    driver: bridge

Note: Add the latest Cloudflare IP ranges explicitly in REVERSE_PROXY_TRUSTED_PROXIES or TRUSTED_PROXIES environment variables to ensure accurate client IP logging and security.

Step 3: Advanced Nginx Configuration

Your Nginx config files must support important features:

  • Proxy headers to pass the correct client IP and protocol.

  • WebSocket support for real-time features.

  • Static file caching to improve performance.

  • Client body size limit to prevent abuse.

  • Error and access logging for troubleshooting.

Example for Gitea (nginx-gitea.conf):

server {
    listen 80;
    server_name git.yourdomain.com;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers HIGH:!aNULL:!MD5;

    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options DENY;
    add_header Referrer-Policy "no-referrer";
    add_header Content-Security-Policy "default-src 'self';";
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    client_max_body_size 100M;

    location / {
        proxy_pass http://gitea: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_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_read_timeout 36000s;
        proxy_send_timeout 36000s;
    }

    location /assets/ {
        root /data/gitea/public/;
        expires max;
    }

    error_log /var/log/nginx/gitea_error.log warn;
    access_log /var/log/nginx/gitea_access.log combined;
}

The Forgejo config is analogous but proxies to http://forgejo:3000/.

Step 4: Cloudflare SSL & Firewall Integration

Use these Cloudflare settings to avoid common SSL errors:

  • SSL/TLS mode: set to Full (strict) to require your VPS to have a valid SSL cert.

  • Enable Always Use HTTPS to redirect all HTTP to HTTPS.

  • Upload your domain's SSL certificates with Certbot (next step) to avoid 525 SSL handshake errors.

Configure your VPS firewall (UFW) to allow inbound HTTP/HTTPS only from Cloudflare's IPs:

curl https://www.cloudflare.com/ips-v4 -o cloudflare-ips-v4.txt
curl https://www.cloudflare.com/ips-v6 -o cloudflare-ips-v6.txt

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp

for ip in $(cat cloudflare-ips-v4.txt); do
  sudo ufw allow from $ip to any port 80,443 proto tcp
  done
for ip in $(cat cloudflare-ips-v6.txt); do
  sudo ufw allow from $ip to any port 80,443 proto tcp
  done

sudo ufw enable
sudo ufw reload

Automation tip: Write a script to periodically update UFW rules when Cloudflare IPs change.

Step 5: SSL Certificates with Certbot & Renewal Automation

Install Certbot and get certificates for your domain:

sudo apt update
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d git.yourdomain.com

Certbot installs certificates in your local certs/ folder (mounted to Nginx).

Set up automatic renewal by editing the root crontab:

sudo crontab -e

Add the following line to renew certs daily and reload Nginx if renewed:

0 3 * * * certbot renew --post-hook "docker-compose -f docker-compose-gitea.yml restart nginx"

Replace docker-compose-gitea.yml with your compose file if different.

Step 6: Important Application-Level Configuration for Gitea & Forgejo

Trusted Proxies

Add Cloudflare's IP ranges to trusted proxies so Gitea/Forgejo log true client IPs and prevent header spoofing.

  • Gitea uses GITEA__server__REVERSE_PROXY_TRUSTED_PROXIES

  • Forgejo uses FORGEJO_APP__server__TRUSTED_PROXIES

You can list IPs or CIDR ranges separated by commas.

Custom Security Settings

Edit app.ini inside your persistent data volume or via UI after first run to:

  • Disable open user registrations

  • Configure SMTP settings for email notifications

Persistent Databases

Although SQLite (default) works for small setups, consider PostgreSQL or MySQL for durability:

Volume Backup

Regularly back up your gitea-data/ or forgejo-data/ folders:

tar czf gitea-backup-$(date +%F).tar.gz gitea-data/

Or use other backup strategies matching your needs.

Step 7: Monitoring and Log Management

Logs

Check logs to troubleshoot errors:

  • Nginx logs inside the container (mapped by default to /var/log/nginx/)

  • Docker container logs:

docker logs -f gitea
# or for forgejo

docker logs -f forgejo

# For nginx

docker logs -f nginx-gitea

Log Rotation

Logs can grow large. Implement log rotation on your VPS:

  • Use system tools like logrotate to compress and delete old logs.

  • Rotate Docker logs by configuring Docker daemon or mounting external log directories.

Automate Cloudflare IP Updates

Cloudflare IPs can change. Automate firewall updates by scripting the download of IP lists and updating UFW rules on a schedule using cron.

Step 8: Troubleshooting Common Issues

  • Push failures or 500 errors often indicate proxy or SSL misconfiguration.

  • Review Nginx error logs and container logs to pinpoint errors.

  • Confirm SSL cert validity and Nginx properly passes HTTP/HTTPS headers.

  • Check that trusted proxies in app settings include Cloudflare IPs, or client IPs will be unknown.

  • If WebSocket features fail, verify proxy_set_header Upgrade and connection headers are set.

When in doubt, restart containers and services after config changes:

docker-compose -f docker-compose-gitea.yml restart
# or

docker-compose -f docker-compose-forgejo.yml restart

Use logs to iteratively identify and fix errors.

Step 9: Starting Your Deployment

From your project directory:

  • To start Gitea:

docker-compose -f docker-compose-gitea.yml up -d
  • To start Forgejo:

docker-compose -f docker-compose-forgejo.yml up -d

Visit https://git.yourdomain.com/ to complete initial web-based setup.

Set up admin users, email notifications, and disable open registrations for security.

Summary

Area
Details & Best Practices

Project Dir Structure

Organize Docker, Nginx configs and persistent volume folders

Docker Compose

Separate files per service, map volumes, set trusted proxies

Nginx Reverse Proxy

Pass proxy headers, WebSocket support, static file caching, error logging

Cloudflare

Full (strict) SSL, Always HTTPS, IP whitelist Firewall + DNS proxying

Firewall

UFW allows only Cloudflare IPs on HTTP/HTTPS ports

SSL Certs

Install and auto-renew with Certbot

App Config (Gitea/Forgejo)

Trusted proxies, disable open registration, persistent DB

Logs & Maintenance

Monitor container and Nginx logs, rotate logs, automate IP updates

Troubleshooting

Use verbose logging, check proxy & SSL settings for errors

Last updated