← Back to DevBytes

How to Set Up a Git Server with Gitea on Ubuntu

What is Gitea?

Gitea is a lightweight, open-source, self-hosted Git service written in Go. It provides a complete platform for hosting Git repositories, complete with a web-based interface, issue tracker, pull request workflow, wiki support, and CI/CD integration via webhooks and actions. Think of it as a lean, fast alternative to GitLab or GitHub that you can run on modest hardware—even a Raspberry Pi.

Originally forked from Gogs in 2016, Gitea has matured into a robust community-driven project with a focus on performance, low resource consumption, and ease of deployment. It ships as a single self-contained binary, making installation and maintenance remarkably simple compared to heavier alternatives that require Ruby, Python, or complex container orchestration.

Why Self-Host Your Git Server?

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Self-hosting a Git server with Gitea offers several compelling advantages for development teams and individual developers alike:

Prerequisites

Before you begin, ensure you have the following in place:

Step 1: Update the System and Install Dependencies

Start by updating your package index and installing the essential packages. Gitea needs Git itself, and we'll want a database. For this tutorial we'll use PostgreSQL for production-grade reliability, but Gitea also supports SQLite (zero-config, great for small teams) and MySQL/MariaDB.

sudo apt update && sudo apt upgrade -y
sudo apt install -y git wget curl gnupg2 ca-certificates lsb-release

Now install PostgreSQL:

sudo apt install -y postgresql postgresql-contrib

Start and enable the PostgreSQL service:

sudo systemctl enable postgresql
sudo systemctl start postgresql

Create a dedicated database user and database for Gitea. Replace securepassword with a strong, randomly generated password—store it in a password manager or secrets vault.

sudo -u postgres psql -c "CREATE USER gitea WITH PASSWORD 'securepassword';"
sudo -u postgres psql -c "CREATE DATABASE giteadb OWNER gitea;"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE giteadb TO gitea;"

Step 2: Create a Dedicated System User for Gitea

Running services under dedicated, unprivileged accounts is a fundamental security practice. Never run Gitea as root.

sudo adduser --system --group --home /var/lib/gitea --gecos 'Gitea Version Control' --disabled-password gitea

Create the directory structure that Gitea expects:

sudo mkdir -p /var/lib/gitea/{custom,data,log}
sudo mkdir -p /var/lib/gitea/data/gitea-repositories
sudo mkdir -p /etc/gitea
sudo mkdir -p /var/log/gitea
sudo chown -R gitea:gitea /var/lib/gitea /etc/gitea /var/log/gitea
sudo chmod 750 /var/lib/gitea /etc/gitea

Step 3: Download and Install the Gitea Binary

Visit the Gitea downloads page to find the latest version. At the time of writing, the latest stable release is 1.22.x. Always download the version that matches your CPU architecture (amd64 for most x86_64 servers).

GITEA_VERSION="1.22.3"
wget -O /tmp/gitea "https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-amd64"

Verify the SHA256 checksum (retrieve the checksum file from the same release page and compare):

wget -O /tmp/gitea.sha256 "https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-amd64.sha256"
cd /tmp
sha256sum -c gitea.sha256 --ignore-missing

Once verified, install the binary with proper permissions:

sudo install -m 755 -o root -g root /tmp/gitea /usr/local/bin/gitea
sudo chmod +x /usr/local/bin/gitea

Confirm the installation:

gitea --version

Step 4: Configure Gitea

Gitea uses an INI-style configuration file located at /etc/gitea/app.ini. Create it with the essential production settings. This configuration tells Gitea where to find the database, how to handle SSH operations, and where to store repositories and logs.

sudo nano /etc/gitea/app.ini

Populate it with the following content, adjusting values to match your environment:

# /etc/gitea/app.ini
# Gitea main configuration file

[default]
RUN_USER = gitea
RUN_MODE = prod

[database]
DB_TYPE = pgsql
HOST = 127.0.0.1:5432
NAME = giteadb
USER = gitea
PASSWD = securepassword
SSL_MODE = disable
LOG_SQL = false

[repository]
ROOT = /var/lib/gitea/data/gitea-repositories
DEFAULT_BRANCH = main
ENABLE_PUSH_CREATE_USER = true

[server]
DOMAIN = git.yourdomain.com
ROOT_URL = https://git.yourdomain.com/
HTTP_PORT = 3000
SSH_DOMAIN = git.yourdomain.com
PROTOCOL = http+https
DISABLE_SSH = false
SSH_PORT = 22
SSH_LISTEN_PORT = 22
OFFLINE_MODE = false
LFS_START_SERVER = true
APP_DATA_PATH = /var/lib/gitea/data

[log]
ROOT_PATH = /var/log/gitea
MODE = file
LEVEL = Info

[service]
REQUIRE_SIGNIN_VIEW = false
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false

[mailer]
ENABLED = false

Set the correct ownership and restrictive permissions on the configuration file—it contains database credentials:

sudo chown gitea:gitea /etc/gitea/app.ini
sudo chmod 640 /etc/gitea/app.ini

Step 5: Create a systemd Service Unit

A systemd service ensures Gitea starts on boot, restarts automatically after crashes, and integrates cleanly with Ubuntu's init system.

sudo nano /etc/systemd/system/gitea.service

Add the following service definition:

# /etc/systemd/system/gitea.service
[Unit]
Description=Gitea - A self-hosted Git service
After=network.target postgresql.service
Requires=postgresql.service
Documentation=https://docs.gitea.com

[Service]
Type=simple
User=gitea
Group=gitea
WorkingDirectory=/var/lib/gitea
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
Environment=USER=gitea HOME=/var/lib/gitea GITEA_WORK_DIR=/var/lib/gitea
Restart=always
RestartSec=3
LimitNOFILE=65535
MemoryAccounting=true
CPUWeight=200

[Install]
WantedBy=multi-user.target

Reload systemd, enable the service, and start it:

sudo systemctl daemon-reload
sudo systemctl enable gitea.service
sudo systemctl start gitea.service

Check that the service is running without errors:

sudo systemctl status gitea.service
sudo journalctl -u gitea.service -f

At this point, Gitea is listening on port 3000 on localhost. You can verify with:

curl http://localhost:3000

Step 6: Set Up Nginx as a Reverse Proxy with SSL

Exposing Gitea directly on port 3000 is fine for testing, but production deployments demand TLS termination and proper HTTP headers. Nginx handles this elegantly and can also serve as a load balancer if needed later.

Install Nginx:

sudo apt install -y nginx

Create a site configuration:

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

Here's a complete Nginx configuration that handles HTTPS termination, WebSocket upgrades (for live notifications), and proper proxy headers:

# /etc/nginx/sites-available/gitea
server {
    listen 80;
    listen [::]:80;
    server_name git.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name git.yourdomain.com;

    # SSL certificates — adjust paths to your actual cert locations
    ssl_certificate     /etc/letsencrypt/live/git.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/git.yourdomain.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 10m;

    # Proxy all requests to Gitea's internal port
    location / {
        proxy_pass http://127.0.0.1:3000;
        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;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400;
        proxy_buffering off;
        proxy_redirect off;

        # Increase upload size limit for large repositories
        client_max_body_size 512m;
    }

    # Serve static assets directly (optional performance boost)
    location ^~ /assets/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        expires 7d;
        add_header Cache-Control "public, immutable";
    }

    # Deny access to hidden files
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    access_log /var/log/nginx/gitea_access.log;
    error_log  /var/log/nginx/gitea_error.log;
}

Enable the site and test the configuration:

sudo ln -s /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Obtain SSL Certificates with Certbot

If you haven't already secured SSL certificates, install Certbot and obtain a Let's Encrypt certificate:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d git.yourdomain.com

Certbot will automatically modify your Nginx configuration to point to the correct certificate paths. After this, your Gitea instance should be accessible at https://git.yourdomain.com.

Step 7: Initial Gitea Setup Through the Web Interface

Navigate to your Gitea URL in a browser. The first visit triggers the installation wizard. Most settings are pre-populated from your app.ini configuration, but you'll need to confirm a few things:

Click "Install Gitea" at the bottom of the page. The system will initialize the database schema and redirect you to the login page. Sign in with your admin credentials.

Step 8: SSH Configuration for Git Operations

Gitea can handle SSH operations in two modes. The default built-in mode uses the system SSH daemon and works transparently when Gitea runs as the gitea user. For most deployments, no extra configuration is needed—users push and pull via SSH just like they would with GitHub or GitLab.

However, there's an important consideration: when users connect via SSH, the command must be routed through Gitea's authorizations. Gitea ships with an gitea serv subcommand that handles this. Create a symlink to make it available as an SSH shell:

sudo ln -s /usr/local/bin/gitea /usr/local/bin/gitea-receive
sudo ln -s /usr/local/bin/gitea /usr/local/bin/gitea-upload-pack
sudo ln -s /usr/local/bin/gitea /usr/local/bin/gitea-upload-archive

Verify SSH access works by testing from a remote machine:

ssh -T git@git.yourdomain.com

You should see a message indicating successful authentication through Gitea, listing your registered SSH keys and repositories.

Step 9: Adding SSH Keys and Creating Your First Repository

Once logged into the web interface, click your avatar in the top-right corner and navigate to Settings → SSH / GPG Keys. Paste your public SSH key into the text area and click "Add Key." This key is now authorized for Git operations.

To create your first repository, click the "+" icon in the top navigation bar or use the "New Repository" button on your dashboard. Fill in the repository name, choose visibility (public or private), and optionally initialize with a README and .gitignore template.

Clone the repository locally:

git clone git@git.yourdomain.com:yourusername/your-repo.git
cd your-repo
echo "# My Project" >> README.md
git add README.md
git commit -m "Initial commit"
git push -u origin main

Your self-hosted Git server is now fully operational. Repeat this workflow for every new project your team creates.

Configuring Gitea Actions (CI/CD)

Gitea includes a built-in CI/CD system called Actions, compatible with GitHub Actions syntax. To enable it, first start the act runner on a machine (it can be the same server or a dedicated worker):

# On a dedicated runner machine, or the same server
wget -O /tmp/act-runner "https://dl.gitea.com/act_runner/0.2.11/act_runner-0.2.11-linux-amd64"
sudo install -m 755 /tmp/act-runner /usr/local/bin/act-runner

Register the runner with your Gitea instance by obtaining a registration token from the web UI under Site Administration → Actions → Runners, then run:

act-runner register --instance https://git.yourdomain.com --token YOUR_TOKEN --name my-runner
act-runner daemon

Now any repository with a .gitea/workflows/ directory containing YAML workflow files will execute automated tests, builds, and deployments on every push or pull request.

Best Practices for Running Gitea in Production

1. Regular Backups Are Non-Negotiable

Gitea stores repositories as plain Git directories on disk, plus metadata (issues, users, pull requests) in the database. Both must be backed up together. Use Gitea's built-in dump command:

sudo -u gitea gitea dump --config /etc/gitea/app.ini --file /backups/gitea-dump-$(date +%Y%m%d).zip

This creates a ZIP containing all repositories, database data, and configuration. Schedule it via cron:

0 2 * * * sudo -u gitea gitea dump --config /etc/gitea/app.ini --file /backups/gitea-dump-$(date +%Y%m%d).zip

Store backups off-server—push them to cloud object storage or a separate machine. A backup that lives on the same physical server as your production data is not a real backup.

2. Keep Gitea Updated

Gitea releases frequently with security fixes and new features. Subscribe to the release feed or set up a monthly maintenance window. To upgrade, simply replace the binary and restart:

# Download new version
wget -O /tmp/gitea-new "https://dl.gitea.com/gitea/1.22.4/gitea-1.22.4-linux-amd64"
# Verify checksum, then install
sudo systemctl stop gitea
sudo install -m 755 /tmp/gitea-new /usr/local/bin/gitea
sudo systemctl start gitea

Always run gitea doctor after upgrades to detect configuration mismatches or database schema issues.

3. Secure SSH with Minimal Access

If your team only needs HTTP/HTTPS access (pulling and pushing over TLS), you can disable SSH entirely in app.ini by setting DISABLE_SSH = true. If SSH is required, consider restricting the Gitea system user's shell to only allow Git operations:

sudo usermod -s /usr/local/bin/gitea gitea

This prevents the gitea user from executing arbitrary commands via SSH.

4. Monitor Logs and Resource Usage

Set up log rotation for Gitea's logs to prevent disk exhaustion:

# /etc/logrotate.d/gitea
/var/log/gitea/*.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 640 gitea gitea
    postrotate
        systemctl kill -s USR1 gitea.service
    endscript
}

Monitor memory and CPU with systemd-cgtop or integrate with your existing monitoring stack (Prometheus, Grafana, Netdata). Gitea exposes metrics at /metrics when enabled.

5. Enforce TLS and Strong Ciphers

Always serve Gitea over HTTPS. Redirect all HTTP traffic permanently (301). Test your SSL configuration with tools like SSL Labs and adjust ciphers to maintain an A+ rating.

6. Use Environment Variables for Secrets

Never hard-code database passwords or API keys in app.ini if the file might end up in version control. Gitea supports environment variable substitution. For example:

[database]
PASSWD = ${GITEA_DB_PASSWORD}

Then set the variable in the systemd service file with Environment=GITEA_DB_PASSWORD=securepassword (or better, use a systemd EnvironmentFile with restricted read permissions).

Conclusion

You've now deployed a fully functional, production-ready Git server using Gitea on Ubuntu. You have a web interface for repository management, SSH-based Git operations, a reverse proxy with TLS termination, and a foundation for CI/CD pipelines through Gitea Actions. The setup consumes minimal resources, gives you complete control over your code, and can scale from a solo developer's Raspberry Pi to a team of hundreds on cloud infrastructure.

The key to long-term success with a self-hosted Git service is consistent maintenance: automate backups, test them regularly, stay current with Gitea releases, and monitor your instance's health. With these practices in place, Gitea becomes a quietly dependable piece of infrastructure that your development workflow can rely on for years to come.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles