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 pushor 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?
- Privacy is paramount: When you host Gitea, your code sits on your disk. No AI training on your private repos, no third-party telemetry.
- 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.
- 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 --versionIf 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 effectStep 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 backupsStep 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.ymlPaste 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_passwordin both thedbandgiteasections. - 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
2222on the host to port22inside 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 giteaIf 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:
- Database Settings: These should be auto-filled based on your docker-compose environment variables. Leave them as is.
- 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.
- SSH Settings:
- SSH Server Port: Set this to
2222. This is crucial so Gitea generates the correct clone URLs.
- SSH Server Port: Set this to
- 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 -yStep 2: Configure the server block
Create a new configuration file for your Gitea instance.
sudo nano /etc/nginx/sites-available/giteaPaste 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 nginxNow, 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.comFollow 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.gitThis 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 git2. 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/gitea3. 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/.sshRestart 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.
- Log in to your new Gitea website.
- Click the + icon in the top right -> New Repository.
- Name it
my-first-project. Check “Initialize Repository”. - 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 pushIf 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=truein yourdocker-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 enableNotice 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.shNow, add it to your crontab to run every night at 2 AM:
crontab -e
# Add this line:
0 2 * * * /root/gitea/backup.shDatabase 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.
- Dump your data:
docker compose exec gitea gitea dump -c /data/gitea/conf/app.ini. - Stop the container.
- Change
docker-compose.ymlto include Postgres (as shown in Step 3). - Gitea has a built-in
gitea restorecommand, 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!








