Deploying a Self-Hosted Git Server with Gitea in Docker

Deploying a Self-Hosted Git Server with Gitea in Docker

If you're tired of relying on GitHub for every private project, or you want complete control over your source code repositories, a self-hosted Git server is the answer. I've been running Gitea in Docker for the past year, and it's genuinely become my favorite deployment—it's lightweight, feature-rich, and doesn't require the resource overhead of something like GitLab.

This tutorial walks you through deploying Gitea on a VPS or homelab using Docker Compose, configuring it behind a reverse proxy, and setting up backups so your code stays safe.

Why Gitea Over Other Git Servers?

When I first looked at self-hosted Git options, I tested GitLab, Forgejo, and Gitea. GitLab is powerful but requires 4GB+ RAM just to breathe. Forgejo is excellent but newer and less tested in production. Gitea, meanwhile, runs comfortably in under 512MB of RAM, has a polished UI, and includes everything I need: webhooks, organizations, CI/CD integration, and issue tracking.

For a VPS around $40–50/year (think RackNerd or similar budget providers), Gitea feels like overkill in the best way. You get professional-grade Git hosting on hardware smaller than a router.

Prerequisites

Docker Compose Setup for Gitea

I prefer Docker Compose because it handles networking, persistence, and updates cleanly. Here's my production-tested configuration that includes PostgreSQL for the database and Gitea itself:

version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: gitea_db
    environment:
      POSTGRES_USER: gitea
      POSTGRES_PASSWORD: your_secure_password_here
      POSTGRES_DB: gitea
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped
    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_app
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      USER_UID: 1000
      USER_GID: 1000
      GITEA__database__DB_TYPE: postgres
      GITEA__database__HOST: postgres:5432
      GITEA__database__NAME: gitea
      GITEA__database__USER: gitea
      GITEA__database__PASSWD: your_secure_password_here
      GITEA__security__INSTALL_LOCK: "true"
      GITEA__server__ROOT_URL: https://git.yourdomain.com
      GITEA__server__SSH_DOMAIN: git.yourdomain.com
      GITEA__server__SSH_PORT: 22
      GITEA__server__DISABLE_SSH: "false"
    ports:
      - "3000:3000"
      - "2222:22"
    volumes:
      - gitea_data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    restart: unless-stopped
    networks:
      - gitea_network

volumes:
  gitea_data:
  postgres_data:

networks:
  gitea_network:
    driver: bridge

Save this as docker-compose.yml in a new directory called gitea/. Replace your_secure_password_here with a strong password—use openssl rand -base64 32 to generate one. Update git.yourdomain.com to your actual domain.

Tip: I map port 2222 for Git SSH instead of 22, because most VPS providers block outbound SSH. Users will clone with git clone ssh://[email protected]:2222/user/repo.git. If you control your network, you can use port 22 instead.

Start the stack with:

cd gitea/
docker compose up -d

Wait 30 seconds, then check the logs:

docker compose logs -f gitea_app

You should see Gitea initializing. Once you see Starting new Web server, it's running on http://localhost:3000.

Reverse Proxy Configuration with Caddy

I use Caddy because it handles HTTPS automatically. If you haven't installed Caddy yet, grab it via your package manager or Docker. Here's my Caddyfile entry:

git.yourdomain.com {
    reverse_proxy localhost:3000 {
        header_uri -Path /.well-known/acme-challenge
        header_down Strict-Transport-Security max-age=31536000
    }
}

Reload Caddy with caddy reload (if running systemd) or restart the container. Caddy will automatically fetch an SSL certificate and proxy traffic to Gitea.

Initial Gitea Configuration

Navigate to https://git.yourdomain.com in your browser. You'll see the installation screen. Here's what I configure:

Click "Install Gitea" and wait. The app restarts automatically after setup. You'll then log in with your admin credentials.

Creating Your First Repository

Once logged in, click the "+" icon in the top-right and select "New Repository." I usually keep them private by default. After creation, you'll see clone instructions:

Both work; SSH is passwordless if you upload your public key in Settings > SSH Keys. I recommend setting that up for local machines:

cat ~/.ssh/id_rsa.pub
# Copy the output and paste it in Gitea settings

Enabling SSH Key Authentication

To use SSH without passwords, upload your public key. On your local machine:

ssh-keygen -t ed25519 -C "my-machine"
# Press Enter to accept defaults, skip passphrase for CI/CD use
cat ~/.ssh/id_ed25519.pub

Log into Gitea, go to Settings > SSH Keys > Add Key, paste the output, and save. Test with:

ssh -p 2222 [email protected]
# Should show: Hi username! You've successfully authenticated, but Gitea does not provide shell access.
Watch out: If you change the SSH port in docker-compose.yml, update GITEA__server__SSH_PORT and the port mapping. The port inside the container is always 22; we remap it to 2222 externally. Gitea needs to know the external port to generate clone URLs correctly.

Backing Up Your Repositories

I run a simple backup script weekly. Gitea includes a built-in dump command that exports everything—repos, users, issues, and config. Add this to your cron:

#!/bin/bash
# /home/user/gitea-backup.sh

BACKUP_DIR="/mnt/backups/gitea"
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p "$BACKUP_DIR"

# Dump the entire Gitea database and data
docker exec gitea_app bash -c "/usr/local/bin/gitea dump -c /data/gitea/conf/app.ini" > "$BACKUP_DIR/gitea_dump_$DATE.zip"

# Keep only the last 4 backups
cd "$BACKUP_DIR"
ls -t gitea_dump_*.zip | tail -n +5 | xargs -r rm

echo "Gitea backup completed: gitea_dump_$DATE.zip"

Add to root's crontab (crontab -e):

0 2 * * 0 /home/user/gitea-backup.sh >> /var/log/gitea-backup.log 2>&1

This runs every Sunday at 2 AM and keeps 4 backups. The dump command produces a ZIP file with everything needed to restore Gitea.

Updates and Maintenance

Gitea updates are painless with Docker. Just pull the latest image and restart:

cd gitea/
docker compose pull
docker compose up -d

The database migrates automatically on startup. I check the release notes before updating just to be safe, but in my experience, Gitea rarely breaks things.

Advanced: Webhooks and Integration

Once you're comfortable, Gitea's webhook system is powerful. In any repository Settings > Webhooks, you can trigger external scripts or CI/CD pipelines. I use this to deploy changes automatically—a simple POST request to a listener script on my VPS:

#!/bin/bash
# Simple webhook receiver (run with a web server like socat or uWSGI)
# Triggered on push, pulls latest code and restarts a service

REPO_PATH="/var/www/my-app"

cd "$REPO_PATH"
git pull origin main
systemctl restart my-app-service

This creates a lightweight CI/CD loop without needing heavy runners.

Performance and Resource Usage

On my 2GB RAM VPS, Gitea typically uses:

For comparison, a GitHub-scale deployment isn't necessary. Even with 20 repositories and a dozen users, my setup stays under 1GB RAM.

Next Steps

You now have a fully functional, SSL-secured Git server. From here, I'd recommend:

Gitea is my go-to because it respects minimalism—no bloat, no mandatory cloud integration, just Git. Whether you're hosting code privately or running a small team, this setup scales beautifully and keeps your code under your complete control.

Discussion