Building a Private Git Server with Gitea: Self-Hosted Version Control
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
If you're managing code for a team, personal projects, or production services, relying on GitHub—or paying for private repositories—isn't your only option. I've been running a private Gitea instance on my homelab for the past year, and it's been rock solid: lightweight, feature-complete, and completely under my control. Gitea runs comfortably on a $40/year budget VPS from providers like RackNerd, or directly on your existing Docker infrastructure.
In this tutorial, I'll walk you through deploying Gitea in Docker, configuring SSH access, setting up reverse proxy access, and integrating Gitea Actions for CI/CD pipelines. By the end, you'll have a production-ready Git server that supports pull requests, webhooks, and team collaboration—all self-hosted.
Why Gitea Over GitHub or Gitlab?
Gitea is purpose-built for self-hosting. It's a lightweight fork of Gogs that includes modern features like pull requests, teams, webhooks, and Actions support—but without the resource overhead of GitLab or the cloud lock-in of GitHub.
I chose Gitea because it runs in under 100MB of RAM, deploys in seconds via Docker, and requires minimal maintenance. It also supports repository mirroring, which is perfect for backing up public repositories or syncing private code between environments. Plus, no one else holds the keys to your repositories.
If you need a VPS for Gitea, RackNerd regularly offers excellent deals around $40/year for 2GB RAM and 50GB SSD—more than enough to run Gitea alongside other services like Caddy or Pi-hole.
Prerequisites
- A Linux server (VPS or homelab machine) with Docker and Docker Compose installed
- A reverse proxy (Caddy or Nginx) for HTTPS access
- A domain name (e.g., git.example.com)
- Basic familiarity with Docker Compose and Git
Deploying Gitea with Docker Compose
I prefer Docker Compose for Gitea because it keeps the container, database, and volumes organized in a single configuration file. Here's a production-ready setup:
version: '3.8'
services:
gitea-db:
image: postgres:15-alpine
container_name: gitea-db
restart: always
environment:
POSTGRES_DB: gitea
POSTGRES_USER: gitea
POSTGRES_PASSWORD: your_secure_password_here
volumes:
- gitea-db-data:/var/lib/postgresql/data
networks:
- gitea-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitea"]
interval: 10s
timeout: 5s
retries: 5
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: always
depends_on:
gitea-db:
condition: service_healthy
ports:
- "3000:3000"
- "2222:22"
environment:
- DB_TYPE=postgres
- DB_HOST=gitea-db
- DB_NAME=gitea
- DB_USER=gitea
- DB_PASSWD=your_secure_password_here
- GITEA_http_DOMAIN=git.example.com
- GITEA_HTTP_PORT=3000
- GITEA_ROOT_URL=https://git.example.com
- GITEA_SSH_PORT=2222
- GITEA_DISABLE_REGISTRATION=false
volumes:
- gitea-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
networks:
- gitea-network
volumes:
gitea-data:
gitea-db-data:
networks:
gitea-network:
driver: bridge
your_secure_password_here to a strong, random password. I generate mine with openssl rand -base64 32. Also update git.example.com to your actual domain.Deploy it:
docker compose up -d
Gitea will be available on port 3000. The first user to sign up becomes the admin. Visit http://localhost:3000, create your account, and proceed to initial setup. You'll configure application name, site URL, and SSH port there as well.
Setting Up Reverse Proxy Access with Caddy
I use Caddy for reverse proxy duties because it auto-renews SSL certificates and handles HTTP/2 Server Push. Here's my Caddyfile configuration:
git.example.com {
reverse_proxy localhost:3000 {
header_uri -X-Real-IP
header_up X-Real-IP {http.request.remote.host}
header_up X-Forwarded-For {http.request.remote.host}
header_up X-Forwarded-Proto {http.request.scheme}
}
encode gzip
log {
output file /var/log/caddy/git.example.com.log
}
}
Reload Caddy to apply the configuration:
sudo systemctl reload caddy
Caddy will automatically provision a Let's Encrypt certificate. Visit https://git.example.com and you're live on HTTPS.
GITEA_SSH_PORT in the Docker Compose file matches the port you expose on your host. If you're using port 2222 for SSH (as in my example), users will clone via git clone ssh://[email protected]:2222/username/repo.git.Configuring SSH Access
SSH is critical for seamless Git workflows. The Gitea container exposes port 2222, but users need to configure their SSH client to use the correct port. Create a file in your user's ~/.ssh/config:
Host git.example.com
HostName git.example.com
Port 2222
User git
IdentityFile ~/.ssh/id_ed25519
Now users can clone repositories without specifying the port:
git clone [email protected]:username/repo.git
To add an SSH key, log in to Gitea, go to Settings → SSH Keys, and paste your public key. Generate one if needed:
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519
Creating Repositories and Teams
Once logged in as the admin, creating a repository is straightforward. I always set repositories to private by default, then selectively open them for team collaboration.
To invite team members:
- Go to Organization → Teams (or create an organization first)
- Add users to the team and assign permissions (read, write, admin)
- Add the team to repositories
Gitea supports three permission levels: Read (clone only), Write (push), and Admin (manage settings, delete). This granularity works perfectly for small teams and enforces code review workflows.
Setting Up Gitea Actions for CI/CD
Gitea Actions is a lightweight CI/CD system compatible with GitHub Actions workflows. Enable it in your admin panel (Admin → Actions). Here's a simple example that runs tests on every push:
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- run: npm install
- run: npm test
Save this as .gitea/workflows/test.yml in your repository. Gitea will execute the workflow whenever you push code.
Backup and Recovery
Gitea includes a built-in backup tool. I run this monthly:
docker exec gitea gitea dump -c /data/gitea/conf/app.ini
This creates a tarball in the Gitea data directory. Back it up to cold storage (S3, Backblaze, or external drive). To restore from a backup, extract the tarball and restart Gitea.
Monitoring and Maintenance
Monitor Gitea's health by checking logs:
docker logs -f gitea
Update Gitea regularly by pulling the latest image:
docker compose pull && docker compose up -d
I set up Watchtower to auto-update Gitea weekly, but some prefer manual control for critical services.
Performance and Scaling
On a $40/year VPS with 2GB RAM, Gitea comfortably handles 5–10 active users and hundreds of repositories. If you need to scale, consider:
- Larger VPS: Move to 4GB RAM for 50+ users
- External PostgreSQL: Use a managed database service like AWS RDS or DigitalOcean Postgres
- Load balancing: Run multiple Gitea instances behind a load balancer (requires shared database and storage)
Conclusion
Gitea has been a game-changer for my workflow. It's lightweight, reliable, and gives me full control over my code. Setting it up took less than 30 minutes, and I haven't had to touch it since (except for routine updates).
If you're ready to own your repositories, start with a basic Docker Compose deployment on your homelab or a budget VPS. Once you've got it running, add team members, mirror your critical repositories, and let Gitea do the heavy lifting. Next steps: enable Gitea Actions for CI/CD, set up repository webhooks for deployments, or integrate Gitea with an identity provider like Authentik for SSO.
Discussion