SELF-HOSTING

Build Your Own Code Fortress: The Ultimate Gitea on Docker Guide

Let’s be honest for a second. If you are a developer or a DevOps engineer, you’ve probably felt the squeeze of the current Git hosting landscape. GitHub is fantastic, but it’s a walled garden where you don’t own the soil. GitLab is powerful, but installing it on a personal server feels like trying to park a semi-truck in a compact garage—it’s resource-hungry and often overkill for smaller teams.

Enter Gitea.

If you are looking for a platform that feels like GitHub but runs on a toaster, this is it. Written in Go, Gitea is the “Goldilocks” solution of the self-hosted world. It’s incredibly lightweight (idling at a mere 50–150 MB of RAM), yet it packs every feature you actually use: Pull Requests, Issue Tracking, CI/CD integration, and releases.

In this guide, I’m not just going to show you how to install it. I’m going to walk you through building a production-ready Gitea instance using Docker Compose. We will cover everything from the database backend and Nginx reverse proxy to SSL security and automated backups. By the end of this tutorial, you will have a code forge that is entirely yours.

What is Gitea and why should you care?

Before we start typing commands, it is crucial to understand what makes Gitea special. It is a community-managed fork of Gogs, designed to be painless to set up and easy to maintain.

The technical edge

  • Performance: Unlike Java or Ruby-based alternatives, Gitea is compiled into a single binary. It starts instantly and runs smoothly on everything from a Raspberry Pi to a high-end Xeon server.
  • Compatibility: It supports the standard Git protocols (HTTP/HTTPS/SSH), so your workflow with git push or VS Code doesn’t change at all.
  • Database support: It plays nice with SQLite (for simple setups), but also scales effortlessly with MySQL and PostgreSQL.

Why self-host?

  1. Privacy is paramount: When you host Gitea, your code sits on your disk. No AI training on your private repos, no third-party telemetry.
  2. Cost efficiency: GitHub charges per user for advanced features. Gitea is free. You can host 1,000 repositories and 50 users on a $5/month VPS.
  3. No “sunset” fears: You control the updates. You control the backups. No one can pull the plug on your service.

Prerequisites: Preparing the ground

To ensure this setup is robust, we aren’t cutting corners. Here is what you need.

Hardware requirements

  • Server: A VPS (like DigitalOcean, Linode, Hetzner) or a local home lab server.
  • RAM: Minimum 1 GB (Gitea uses little, but we are also running Docker and a Database).
  • Storage: 20 GB+ recommended. Git repos grow over time!
  • OS: Ubuntu 20.04/22.04 LTS is the golden standard, but any Docker-capable Linux distro works.

Software checklist

You need Docker and Docker Compose installed. If you haven’t done this yet, run the following verification commands to ensure you are ready to go.

# Check Docker
docker --version

# Check Docker Compose
docker compose version

# If you get an error, use the legacy syntax:
docker-compose --version

If you don’t have them, please refer to the official Docker installation guide before proceeding.

Step-by-step deployment: The core setup

We are going to use Docker Compose. This is the professional way to manage deployments because it makes your infrastructure reproducible. We will also use PostgreSQL instead of SQLite because it offers better data integrity and concurrency for the long term.

Step 1: Prepare the server envrionment

First, log into your server and update your package lists. We also need git and curl for basic operations.

# Update system packages
sudo apt update && sudo apt upgrade -y

# Install required tools
sudo apt install git curl wget -y

# Verify Docker is running
sudo systemctl status docker
sudo usermod -aG docker $USER
# Log out and log back in for group changes to take effect

Step 2: Create the directory structure

Organization is key. We will create a dedicated folder for Gitea and subfolders to map our data volumes. This ensures that even if you delete the containers, your data (repositories and database) remains safe on the host.

# Create a directory for Gitea
mkdir -p ~/gitea && cd ~/gitea

# Create subdirectories for persistent data
mkdir -p gitea postgres backups

Step 3: The docker-compose configuration

This is the most critical part. We will create a docker-compose.yml file that orchestrates the database and the application.

Open the file:

nano docker-compose.yml

Paste the following configuration. I have optimized this for a standard production environment:

networks:
  gitea:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16

volumes:
  gitea-data:
    driver: local
  postgres-data:
    driver: local

services:
  # PostgreSQL Database
  db:
    image: docker.io/library/postgres:14-alpine
    container_name: gitea-db
    restart: always
    networks:
      - gitea
    environment:
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=change_this_to_a_strong_password
      - POSTGRES_DB=gitea
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./backups:/backups
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U gitea"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Gitea Server
  gitea:
    image: docker.gitea.com/gitea:1.25.3
    container_name: gitea
    restart: always
    networks:
      - gitea
    depends_on:
      db:
        condition: service_healthy
    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=change_this_to_a_strong_password
      - GITEA__server__ROOT_URL=http://localhost:3000
      - GITEA__server__SSH_DOMAIN=localhost
      - GITEA__server__DISABLE_SSH=false
      - GITEA__server__SSH_PORT=2222
      - GITEA__server__SSH_LISTEN_PORT=22
    volumes:
      - gitea-data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "127.0.0.1:3000:3000"
      - "127.0.0.1:2222:22"

Critical configuration details:

  • Passwords: You MUST change change_this_to_a_strong_password in both the db and gitea sections.
  • Ports: Notice we are binding ports to 127.0.0.1. This is a security best practice. We do not want Gitea exposed directly to the wild internet; we want traffic to go through Nginx (which we will set up next).
  • SSH: We are mapping port 2222 on the host to port 22 inside the container. This prevents conflict with your server’s real SSH daemon.

Step 4: Launch the stack

It is time to fire it up.

# Launch Gitea and PostgreSQL in background
docker compose up -d

# Verify containers are running
docker compose ps

# Check logs (useful for debugging)
docker compose logs -f gitea

If you see running status for both containers, you are in business. Press Ctrl+C to exit the log view.

Step 5: The installation wizard

Gitea has a web-based installer for the final touches. Open your browser and go to http://your-server-ip:3000.

Fill in the details as follows:

  1. Database Settings: These should be auto-filled based on your docker-compose environment variables. Leave them as is.
  2. General Settings:
    • Site Title: Give your forge a cool name (e.g., “DevOps Hub”).
    • Gitea Base URL: If you have a domain, use https://gitea.example.com/. If not, use your IP for now.
  3. SSH Settings:
    • SSH Server Port: Set this to 2222. This is crucial so Gitea generates the correct clone URLs.
  4. Administrator Account:
    • Important: Create your admin user now. If you skip this, the first person to register becomes the admin!

Click Install Gitea. The container will restart, and in about a minute, you will be redirected to your dashboard.

Reverse proxy setup: Professional HTTPS with Nginx

Exposing port 3000 directly is fine for testing, but for production, you need SSL (HTTPS) and a proper domain name. We will use Nginx as a reverse proxy.

Step 1: Install Nginx

sudo apt install nginx -y

Step 2: Configure the server block

Create a new configuration file for your Gitea instance.

sudo nano /etc/nginx/sites-available/gitea

Paste the following. Make sure to replace gitea.example.com with your actual domain.

upstream gitea {
    server 127.0.0.1:3000;
}

server {
    listen 80;
    server_name gitea.example.com;
    client_max_body_size 256M;

    location / {
        proxy_pass http://gitea;
        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;

        # WebSocket support (for real-time features)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Note: client_max_body_size 256M allows you to push large files to your repositories without Nginx blocking them.

Step 3: Activate and secure

Enable the site and reload Nginx:

# Create symbolic link to enable the site
sudo ln -s /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/gitea

# Remove default site if it exists (optional but recommended)
sudo rm /etc/nginx/sites-enabled/default

# Test Nginx configuration
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

Now, let’s get that green padlock using Certbot (Let’s Encrypt).

# Install Certbot
sudo apt install certbot python3-certbot-nginx -y

# Obtain SSL certificate (interactive)
sudo certbot --nginx -d gitea.example.com

Follow the prompts. Choose to redirect HTTP to HTTPS when asked. Now, accessing https://gitea.example.com will be secure!

Configuring SSH access (The “Pro” way)

SSH is the preferred way to push code. Since Gitea is in a container, we have two ways to handle this.

Method 1: The simple way (Port 2222)

By default, with our configuration, your clone URLs will look like this:

git clone ssh://[email protected]:2222/username/repo.git

This works perfectly fine, but some users find the extra port number annoying.

Method 2: The advanced SSH passthrough (Port 22)

If you want the clean URL (git clone [email protected]...), you need to trick the host’s SSH server into forwarding requests to the Docker container.

1. Create a git user on the host:

# Create git user on host if not exists
sudo adduser --system --shell /bin/bash --disabled-password git

2. Create a wrapper script:
This script intercepts SSH connections to the host’s git user and forwards them to the container.

# Create the Gitea SSH shim
sudo tee /usr/local/bin/gitea > /dev/null <<'EOF'
#!/bin/sh
ssh -p 2222 -o StrictHostKeyChecking=no [email protected] "SSH_ORIGINAL_COMMAND=\"$SSH_ORIGINAL_COMMAND\" $0 $@"
EOF

# Make it executable
sudo chmod +x /usr/local/bin/gitea

3. Link the SSH keys:
You need to mount the host’s git user .ssh directory into the container so Gitea can write authorized keys there. Update your docker-compose.yml volumes section for the gitea service:

    volumes:
      - gitea-data:/data
      - /home/git/.ssh:/data/git/.ssh

Restart the stack (docker compose down && docker compose up -d). Now you have clean SSH URLs!

Adding your first repository

Let’s test if everything is working.

  1. Log in to your new Gitea website.
  2. Click the + icon in the top right -> New Repository.
  3. Name it my-first-project. Check “Initialize Repository”.
  4. Click Create Repository.

Now, on your local machine, try to clone it:

# Clone via HTTPS
git clone https://gitea.example.com/username/my-first-project.git

# Enter folder and make a change
cd my-first-project
echo "Hello Gitea" >> README.md

# Push changes
git add .
git commit -m "My first commit on self-hosted git"
git push

If the push succeeds, congratulations! You are now independent of GitHub.

Security hardening: Keep the bad guys out

Running a server means you are responsible for security. Do not skip these steps.

1. Disable open registration

Unless you want random strangers hosting their code on your server, turn off public registration.

  • Go to Site Administration > Settings.
  • Uncheck Allow Users to Register.
  • Alternatively, set GITEA__service__DISABLE_REGISTRATION=true in your docker-compose.yml.

2. Configure the Firewall (UFW)

Only allow the ports you absolutely need.

# Default deny incoming
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH (for server management)
sudo ufw allow 22/tcp

# Allow HTTP/HTTPS (for Nginx)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Enable firewall
sudo ufw enable

Notice we did NOT allow port 3000. That port is blocked from the outside world, accessible only via Nginx.

3. Automated database backups

Data loss is not an “if”, it’s a “when”. Let’s set up a script to back up your database and files automatically.

Create the script ~/gitea/backup.sh:

cat > ~/gitea/backup.sh <<'EOF'
#!/bin/bash
BACKUP_DIR="/root/gitea/backups"
DATE=$(date +%Y%m%d_%H%M%S)

# Backup PostgreSQL
docker compose exec -T db pg_dump -U gitea gitea | gzip > "$BACKUP_DIR/gitea_db_$DATE.sql.gz"

# Backup Gitea data
tar -czf "$BACKUP_DIR/gitea_data_$DATE.tar.gz" ./gitea

# Keep only last 30 days of backups
find "$BACKUP_DIR" -name "*.gz" -mtime +30 -delete

echo "Backup completed: $BACKUP_DIR"
EOF

chmod +x ~/gitea/backup.sh

Now, add it to your crontab to run every night at 2 AM:

crontab -e
# Add this line:
0 2 * * * /root/gitea/backup.sh

Database deep dive: SQLite vs. PostgreSQL

You might wonder why we chose PostgreSQL over the default SQLite.

  • SQLite is a file-based database. It is great for testing or single-user instances. However, as soon as you have multiple people pushing code or browsing the UI simultaneously, SQLite can lock up (database locking).
  • PostgreSQL is a client-server database. It is designed for high concurrency. If you plan to use Gitea for anything serious—especially if you plan to integrate CI/CD tools that poll the server frequently—PostgreSQL is mandatory for performance.

Switching later:
If you already started with SQLite, you can migrate.

  1. Dump your data: docker compose exec gitea gitea dump -c /data/gitea/conf/app.ini.
  2. Stop the container.
  3. Change docker-compose.yml to include Postgres (as shown in Step 3).
  4. Gitea has a built-in gitea restore command, though it requires some manual CLI work. It is much easier to start with Postgres from day one.

Conclusion: You are now the captain

You have successfully deployed a fully functional, secure, and backed-up Git service. You now enjoy:

  • Total Control: No more reading Terms of Service updates with anxiety.
  • Performance: A snappy interface that doesn’t eat your RAM.
  • Privacy: Your code is your business, and yours alone.

What’s next?
Consider setting up Gitea Actions (compatible with GitHub Actions) to build your own CI/CD pipelines directly on your server. Or, explore the rich ecosystem of Gitea themes to customize the look of your dashboard.

If you run into issues, check the logs with docker compose logs -f, or drop a comment below. Welcome to the world of self-hosting!

You may also like

Subscribe
Notify of
guest

0 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments