Deploying a Self-Hosted Git Server with Gitea in Docker

Deploying a Self-Hosted Git Server with Gitea in Docker

We earn commissions when you shop through the links on this page, at no additional cost to you.

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