Part 1 of 5

    Gitea on RamNode: Complete Self-Hosted Git Setup

    Replace GitHub with a fully self-hosted Git platform running on a $4/month RamNode VPS. Repos, organizations, SSH keys, and GitHub mirroring.

    Why Self-Host Your Git Server?

    GitHub is excellent, but it comes with trade-offs: per-seat pricing that scales with your team, data stored on third-party infrastructure, limited control over CI/CD minutes, and no native package registry for private packages without paying for GitHub Packages.

    Gitea gives you everything GitHub offers for teams of 1 to 50, running on a single VPS costing $4 to $12 per month. You get unlimited private repositories, built-in CI/CD (Gitea Actions, fully compatible with GitHub Actions syntax), a package registry supporting 20+ formats, project boards, PR workflows, and branch protection rules.

    Requirements

    ComponentMinimumRecommended
    VPS PlanRamNode 512MB RAM / 1 vCPURamNode 1GB RAM / 1 vCPU
    Storage10 GB SSD25 GB SSD
    OSUbuntu 22.04 LTSUbuntu 24.04 LTS
    DockerDocker 24+Docker 26+ with Compose v2
    DomainIP-only worksSubdomain (git.yourdomain.com)

    Cost comparison: GitHub Team costs $4/user/month with limited CI minutes. A RamNode 2GB VPS at $8/month covers an entire dev team with no per-seat fees and no CI minute caps.

    1

    Prepare Your VPS

    Log into your RamNode VPS and install Docker:

    Update system and install Docker
    # Update system packages
    sudo apt update && sudo apt upgrade -y
    
    # Install Docker
    curl -fsSL https://get.docker.com | sh
    sudo usermod -aG docker $USER
    newgrp docker
    
    # Install Docker Compose v2
    sudo apt install docker-compose-plugin -y
    
    # Verify installation
    docker --version
    docker compose version

    Configure the firewall. Port 3000 is the web interface, port 222 is for Git-over-SSH (we avoid 22 to keep system SSH accessible):

    Configure firewall
    sudo ufw allow OpenSSH
    sudo ufw allow 3000/tcp   # Gitea web UI
    sudo ufw allow 222/tcp    # Gitea SSH (Git clone over SSH)
    sudo ufw allow 80/tcp     # HTTP (for Let's Encrypt challenge)
    sudo ufw allow 443/tcp    # HTTPS
    sudo ufw enable
    sudo ufw status
    2

    Install Gitea with Docker Compose

    Create the project directory and Docker Compose file with Gitea and PostgreSQL:

    Create project directory
    mkdir -p /opt/gitea/{data,config,logs}
    cd /opt/gitea
    /opt/gitea/docker-compose.yml
    version: '3.8'
    
    services:
      gitea:
        image: gitea/gitea:latest
        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=changeme_strong_password
          - GITEA__server__DOMAIN=git.yourdomain.com
          - GITEA__server__ROOT_URL=https://git.yourdomain.com
          - GITEA__server__SSH_PORT=222
          - GITEA__server__SSH_LISTEN_PORT=22
          - GITEA__mailer__ENABLED=false
        restart: unless-stopped
        networks:
          - gitea
        volumes:
          - ./data:/data
          - ./config:/etc/gitea
          - ./logs:/var/log/gitea
          - /etc/timezone:/etc/timezone:ro
          - /etc/localtime:/etc/localtime:ro
        ports:
          - '3000:3000'
          - '222:22'
        depends_on:
          - db
    
      db:
        image: postgres:16-alpine
        container_name: gitea-db
        restart: unless-stopped
        environment:
          - POSTGRES_USER=gitea
          - POSTGRES_PASSWORD=changeme_strong_password
          - POSTGRES_DB=gitea
        networks:
          - gitea
        volumes:
          - postgres_data:/var/lib/postgresql/data
    
    networks:
      gitea:
        external: false
    
    volumes:
      postgres_data:

    Security: Replace changeme_strong_password with a randomly generated string. Use: openssl rand -hex 32

    3

    Initial Web Configuration

    Start Gitea and complete the setup wizard:

    Start Gitea
    cd /opt/gitea
    docker compose up -d
    
    # Watch logs during startup
    docker compose logs -f gitea

    Once you see Starting new Web server: tcp:0.0.0.0:3000 in the logs, visit http://YOUR_VPS_IP:3000 to complete the setup wizard:

    • 1. Database Type: PostgreSQL (should auto-populate)
    • 2. Server Domain: Your domain or VPS IP
    • 3. Application URL: Full URL including http/https
    • 4. Administrator Account: Create at the bottom of the page

    Note: The first account registered through the setup wizard automatically becomes the admin. Fill in the Optional Settings → Administrator Account section on the setup page.

    4

    Reverse Proxy with Nginx

    Set up Nginx with Let's Encrypt for HTTPS:

    Install Nginx and Certbot
    sudo apt install nginx certbot python3-certbot-nginx -y
    
    # Create Nginx config for Gitea
    sudo tee /etc/nginx/sites-available/gitea << 'EOF'
    server {
        listen 80;
        server_name git.yourdomain.com;
    
        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;
            client_max_body_size 512m;
        }
    }
    EOF
    
    sudo ln -s /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/
    sudo nginx -t && sudo systemctl reload nginx
    
    # Obtain SSL certificate
    sudo certbot --nginx -d git.yourdomain.com
    5

    Organizations & Repositories

    Organizations in Gitea work identically to GitHub organizations. They act as a namespace that multiple users can share, with fine-grained team permissions:

    • 1. Click the + icon in the top right corner
    • 2. Select New Organization
    • 3. Set visibility (Public or Private)
    • 4. Create teams with read, write, or admin permissions

    Repository Settings Worth Configuring

    • Default Branch: Change from 'master' to 'main' if needed
    • Issues: Enable or disable depending on whether you use an external tracker
    • Wiki: Gitea has a built-in wiki editor with Markdown support
    • Protected Branches: Configure in Settings → Branches (covered in Part 5)
    • Webhooks: Pre-configure a webhook to trigger Dokploy/Coolify deploys (Part 4)
    6

    SSH Key Management

    Gitea SSH runs on port 222. Generate a key and configure your SSH client:

    Generate SSH key
    # Generate Ed25519 key (preferred over RSA)
    ssh-keygen -t ed25519 -C 'your_email@example.com' -f ~/.ssh/gitea_ed25519
    
    # Copy the public key
    cat ~/.ssh/gitea_ed25519.pub

    Add the public key in Gitea: User Settings → SSH/GPG Keys → Add Key. Then configure your SSH client:

    ~/.ssh/config
    Host git.yourdomain.com
        HostName git.yourdomain.com
        User git
        Port 222
        IdentityFile ~/.ssh/gitea_ed25519
        IdentitiesOnly yes
    Test SSH access
    ssh -T git@git.yourdomain.com -p 222
    # Expected output: Hi username! You've successfully authenticated, but Gitea does not provide shell access.
    
    # Clone using SSH
    git clone git@git.yourdomain.com:your-org/your-repo.git
    7

    Mirroring from GitHub

    Gitea can act as a pull mirror for any public or private GitHub repository. Useful for keeping a synchronized backup, gradually migrating teams, or maintaining a read-only copy for your CI/CD pipeline to clone locally.

    Setting Up a Pull Mirror

    • 1. Create a new repository in Gitea
    • 2. Under 'Initialize this Repository', select 'This repository will be a mirror'
    • 3. Enter the GitHub clone URL: https://github.com/your-org/your-repo.git
    • 4. For private repositories, enter a GitHub Personal Access Token in the password field
    • 5. Set the mirror interval (e.g., 8h for every 8 hours)

    Migrating Full Repositories

    For a complete migration (not just mirroring), use the Explore → Migrate option to import everything including issues, PRs, labels, milestones, and wikis:

    • 1. Click + → Migrate Repository
    • 2. Select GitHub as the source
    • 3. Enter your GitHub PAT and repository URL
    • 4. Check which data to migrate: Issues, Pull Requests, Releases, Wiki
    • 5. Click Migrate to start the import

    Tip: Use a GitHub fine-grained PAT scoped to 'Repository: Contents (read-only)' for mirror auth. This limits exposure if the token is ever leaked from your Gitea configuration.

    Note: PR/issue migration maps GitHub user mentions to Gitea users if the usernames match. Create matching usernames in Gitea before migrating if you want proper attribution.

    8

    Automated Backups

    Schedule nightly backups using Gitea's built-in dump command, which creates a complete archive of your repositories, database, and configuration:

    /opt/gitea/backup.sh
    #!/bin/bash
    
    BACKUP_DIR=/opt/gitea/backups
    DATE=$(date +%Y-%m-%d)
    mkdir -p $BACKUP_DIR
    
    # Run Gitea's built-in dump inside the container
    docker exec gitea bash -c "gitea dump -c /etc/gitea/app.ini --type tar.gz -f /tmp/gitea-dump.tar.gz"
    docker cp gitea:/tmp/gitea-dump.tar.gz $BACKUP_DIR/gitea-$DATE.tar.gz
    
    # Also dump the PostgreSQL database separately
    docker exec gitea-db pg_dump -U gitea gitea | gzip > $BACKUP_DIR/postgres-$DATE.sql.gz
    
    # Remove backups older than 14 days
    find $BACKUP_DIR -name '*.gz' -mtime +14 -delete
    
    echo 'Gitea backup complete: ' $BACKUP_DIR/gitea-$DATE.tar.gz
    Schedule daily backup via cron
    chmod +x /opt/gitea/backup.sh
    
    # Add to crontab (runs at 2am daily)
    crontab -e
    # Add this line:
    0 2 * * * /opt/gitea/backup.sh >> /var/log/gitea-backup.log 2>&1

    Troubleshooting Common Issues

    Gitea Container Exits on Startup

    Check logs
    # Check logs for the actual error
    docker compose logs gitea --tail=50
    
    # Most common cause: database not ready yet
    # Fix: Add a depends_on with health check to docker-compose.yml

    SSH Clones Fail with 'Port 222 Connection Refused'

    Verify port mapping
    # Verify port mapping
    docker ps | grep gitea
    # Should show: 0.0.0.0:222->22/tcp
    
    # Check UFW
    sudo ufw status | grep 222

    Large File Push Fails

    app.ini — Large file settings
    # Edit /opt/gitea/config/app.ini
    [server]
    MAX_GIT_DIFF_LINES = 1000
    
    [repository]
    MAX_GIT_DIFF_FILES = 100
    
    # Or use Gitea's LFS (Large File Storage)
    # Enable in repository Settings > Git Hooks

    What's Next

    Part 2 of this series covers setting up CI/CD pipelines using Gitea Actions and Woodpecker CI. You will configure container-native build pipelines triggered by pushes to your Gitea repositories, with automatic Docker image builds and test runs on every commit.