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
| Component | Minimum | Recommended |
|---|---|---|
| VPS Plan | RamNode 512MB RAM / 1 vCPU | RamNode 1GB RAM / 1 vCPU |
| Storage | 10 GB SSD | 25 GB SSD |
| OS | Ubuntu 22.04 LTS | Ubuntu 24.04 LTS |
| Docker | Docker 24+ | Docker 26+ with Compose v2 |
| Domain | IP-only works | Subdomain (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.
Prepare Your VPS
Log into your RamNode VPS 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 versionConfigure the firewall. Port 3000 is the web interface, port 222 is for Git-over-SSH (we avoid 22 to keep system SSH accessible):
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 statusInstall Gitea with Docker Compose
Create the project directory and Docker Compose file with Gitea and PostgreSQL:
mkdir -p /opt/gitea/{data,config,logs}
cd /opt/giteaversion: '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
Initial Web Configuration
Start Gitea and complete the setup wizard:
cd /opt/gitea
docker compose up -d
# Watch logs during startup
docker compose logs -f giteaOnce 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.
Reverse Proxy with Nginx
Set up Nginx with Let's Encrypt for HTTPS:
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.comOrganizations & 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)
SSH Key Management
Gitea SSH runs on port 222. Generate a key and configure your SSH client:
# 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.pubAdd the public key in Gitea: User Settings → SSH/GPG Keys → Add Key. Then configure your SSH client:
Host git.yourdomain.com
HostName git.yourdomain.com
User git
Port 222
IdentityFile ~/.ssh/gitea_ed25519
IdentitiesOnly yesssh -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.gitMirroring 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.
Automated Backups
Schedule nightly backups using Gitea's built-in dump command, which creates a complete archive of your repositories, database, and configuration:
#!/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.gzchmod +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>&1Troubleshooting Common Issues
Gitea Container Exits on Startup
# 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.ymlSSH Clones Fail with 'Port 222 Connection Refused'
# Verify port mapping
docker ps | grep gitea
# Should show: 0.0.0.0:222->22/tcp
# Check UFW
sudo ufw status | grep 222Large File Push Fails
# 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 HooksWhat'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.
