👍 What we like
- ✓Extremely lightweight, requiring only 1 vCPU and 2 GB RAM
- ✓Starts up in seconds due to being written in Go
- ✓Offers Gitea Actions compatible with GitHub workflow syntax
- ✓Complete forge with issues, pull requests, wiki, and CI/CD
- ✓Easy deployment via Docker Compose with PostgreSQL
👎 What to watch
- ✕Requires manual configuration of environment variables
- ✕Needs a dedicated SSH port to avoid conflicts with system SSH
- ✕Requires a reverse proxy for clean HTTPS exposure
- ✕Depends on external Docker images for database and app
📑 Contents ▾
- 01 Prerequisites
- 02 Step 1: Install Docker
- 03 Step 2: Point the DNS
- 04 Step 3: Prepare the network and secrets
- 05 Step 4: The Gitea docker-compose.yml
- 06 Step 5: HTTPS Reverse Proxy (Caddy)
- 07 Step 6: Initial Installation and Admin Account
- 08 Step 7: Configure SSH Cloning
- 09 Step 8: Enable Gitea Actions (CI/CD)
- 10 Step 9: Hardening
- 11 Step 10: Backup
- 12 Troubleshooting
- 13 Final Verification
- 14 FAQ
- · Gitea or Forgejo, which one to choose in 2026?
- · Why expose SSH on port 2222 instead of 22?
- · Is Gitea Actions really compatible with GitHub Actions?
- · How many resources does Gitea consume?
- · Can I migrate my repositories from GitHub or GitLab?
- · Should I use PostgreSQL or SQLite?
- 21 Related Topics
Self-hosting your Git repositories means escaping GitHub’s pricing limits, keeping your source code on your own infrastructure, and having a complete forge (issues, pull requests, wiki, CI/CD) that fits on a small VPS. In 2026, Gitea remains the reference for this: written in Go, it starts up in seconds, consumes a fraction of GitLab’s resources, and now offers Gitea Actions, a CI/CD engine compatible with GitHub workflow syntax.
In this tutorial, we deploy Gitea with Docker and a PostgreSQL database, expose it cleanly over HTTPS behind a reverse proxy, configure SSH for repository cloning, enable Gitea Actions, and then harden and back up the whole setup.
Prerequisites
- A secure VPS running Ubuntu 24.04 LTS. Gitea is very lightweight: 1 vCPU and 2 GB of RAM are sufficient for personal or small team use. A Hetzner CX22 is more than enough. If the server isn’t ready yet, see install and secure an Ubuntu VPS.
- Docker and Docker Compose installed (command in step 1).
- A domain name whose DNS you control. We will use
git.exemple.fr. - Ports 80 and 443 open in UFW, plus a dedicated SSH port (we will use 2222) for git cloning, to avoid conflict with the system SSH on port 22.
- A reverse proxy; we will show Caddy, but Traefik or Nginx Proxy Manager also work.
Step 1: Install Docker
If Docker is not present:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker --version && docker compose version
Step 2: Point the DNS
Create an A record pointing to your VPS IP:
| Type | Name | Value |
|---|---|---|
| A | git.exemple.fr | 203.0.113.10 |
Check propagation:
dig +short git.exemple.fr
Step 3: Prepare the network and secrets
Create the shared Docker network with your reverse proxy (if not already done):
docker network create web
Prepare the folder and database password:
mkdir -p ~/gitea && cd ~/gitea
echo "DB_PASS=$(openssl rand -base64 32 | tr -d '\n')" > .env
chmod 600 .env
Step 4: The Gitea docker-compose.yml
nano docker-compose.yml
services:
gitea:
image: docker.gitea.com/gitea:1.24
container_name: gitea
restart: unless-stopped
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: gitea-db:5432
GITEA__database__NAME: gitea
GITEA__database__USER: gitea
GITEA__database__PASSWD: ${DB_PASS}
# Public domain served by the reverse proxy
GITEA__server__DOMAIN: git.exemple.fr
GITEA__server__ROOT_URL: https://git.exemple.fr/
GITEA__server__SSH_DOMAIN: git.exemple.fr
GITEA__server__SSH_PORT: "2222"
# Disable public registration after admin creation
GITEA__service__DISABLE_REGISTRATION: "true"
volumes:
- gitea_data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "2222:22" # Git SSH (cloning)
networks:
- web
- internal
depends_on:
- gitea-db
gitea-db:
image: docker.io/library/postgres:16-alpine
container_name: gitea-db
restart: unless-stopped
environment:
POSTGRES_USER: gitea
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: gitea
volumes:
- gitea_db:/var/lib/postgresql/data
networks:
- internal
volumes:
gitea_data:
gitea_db:
networks:
web:
external: true
internal:
Important points:
- We configure Gitea directly via environment variables (
GITEA__section__key), making the configuration reproducible without manually editingapp.ini. ROOT_URLis crucial: it is the public HTTPS URL. An error here breaks clone links and webhooks.- SSH is exposed on port 2222 on the host, mapped to port 22 in the container. The server’s administrative SSH remains on its usual port.
- The
internalnetwork isolates PostgreSQL; only Gitea joinswebto be reachable by the proxy.
Launch it:
docker compose up -d
Step 5: HTTPS Reverse Proxy (Caddy)
Add this block to your Caddyfile:
git.exemple.fr {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
reverse_proxy gitea:3000
}
Gitea serves its web interface on port 3000 internally; Caddy resolves gitea:3000 via the web network and handles the Let’s Encrypt certificate automatically. Reload:
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
For a complete Caddy deployment from scratch, follow automatic HTTPS reverse proxy with Caddy and Docker.
Common pitfall: do not map port 3000 in the
ports:section of the compose file. Gitea’s HTTP should never be exposed directly to the Internet; everything goes through Caddy. Only the SSH port (2222) is published, because git over SSH does not pass through the HTTP proxy.
Step 6: Initial Installation and Admin Account
Go to https://git.exemple.fr. The installation wizard appears, pre-filled thanks to the environment variables. Verify that the database type is indeed postgres and that the root URL is correct, then scroll down to the Admin Account section: create your admin user (username, email, strong password). Validate.
Since we set DISABLE_REGISTRATION: "true", no one else can register freely: you will add accounts manually from the admin panel. For a family or restricted team instance, this is the recommended setting.
Step 7: Configure SSH Cloning
Open the dedicated SSH port in the firewall:
sudo ufw allow 2222/tcp
In Gitea, add your SSH public key via Settings → SSH/GPG Keys. You can then clone a repository like this:
git clone ssh://git@git.exemple.fr:2222/votre-user/votre-repo.git
To avoid typing the port every time, add a block to ~/.ssh/config on your workstation:
Host git.exemple.fr
Port 2222
User git
Step 8: Enable Gitea Actions (CI/CD)
Gitea Actions is enabled by default in recent versions. To run workflows, you need to register at least one runner (act_runner). First, get a token from Administration → Actions → Runners → Create Registration Token, then deploy the runner:
services:
runner:
image: docker.gitea.com/act_runner:latest
container_name: gitea-runner
restart: unless-stopped
environment:
GITEA_INSTANCE_URL: https://git.exemple.fr
GITEA_RUNNER_REGISTRATION_TOKEN: "YOUR_TOKEN"
GITEA_RUNNER_NAME: main-runner
volumes:
- runner_data:/data
- /var/run/docker.sock:/var/run/docker.sock
volumes:
runner_data:
Launch it (docker compose up -d). The runner will then appear as “Idle” in the interface. You can now add workflows in .gitea/workflows/ in your repositories, with syntax nearly identical to GitHub Actions.
Security note: mounting
/var/run/docker.sockgives the runner full access to the host’s Docker daemon. For an instance exposed to untrusted contributors, isolate the runner on a separate VM or host.
Step 9: Hardening
- Force HTTPS everywhere (already handled by Caddy with HSTS). Check your score on SSL Labs.
- Disable open registration (
DISABLE_REGISTRATION: "true", already done) and enable MFA on your admin account in the security settings. - Limit outgoing emails if you don’t have SMTP: disable email notifications rather than exposing an open relay.
- Restrict organization creation to administrators via
GITEA__service__DEFAULT_ALLOW_CREATE_ORGANIZATION: "false". - Keep the image updated. Monitor Gitea minor versions (frequent security patches) and update the tag after backing up.
Step 10: Backup
Gitea provides a dump command that archives the database, repositories, and configuration into a single file:
docker compose exec -u git gitea gitea dump -c /data/gitea/conf/app.ini -f /data/gitea-dump.zip
Retrieve the archive from the volume, then copy it off-site. Coupled with object storage, this is a solid recovery strategy; our tutorial automatic encrypted backup with restic and Backblaze shows how to automate sending this dump offsite. Also remember to back up the gitea_db volume separately for fine-grained PostgreSQL restoration.
Troubleshooting
- Clone links show
localhostor the wrong port.ROOT_URL,SSH_DOMAIN, orSSH_PORTare misconfigured. Correct the variables and restart the container. Permission denied (publickey)on SSH clone. Either the port is not 2222 in your command, the key is not added to your Gitea account, or UFW is blocking port 2222.- 502 Error via proxy. Caddy cannot reach
gitea:3000: check the sharedwebnetwork and ensure the container is running (docker logs gitea). - Gitea cannot connect to the database. The
DB_PASSdiffers between the two services, or PostgreSQL hasn’t finished starting. Checkdocker logs gitea-db. - Actions remain “Waiting”. No runner is registered or its token has expired. Regenerate a token and restart the runner.
Final Verification
# Containers are running
docker compose ps
# Interface responds over HTTPS
curl -sI https://git.exemple.fr | head -1
# Git SSH port is open
nc -zv git.exemple.fr 2222
Create a first repository, clone it via SSH, push a commit: your git forge is operational.
FAQ
Gitea or Forgejo, which one to choose in 2026?
Forgejo is a community fork of Gitea born from a disagreement on governance; both remain very close technically. Gitea benefits from sustained development and mature Gitea Actions; Forgejo focuses on non-profit governance and federation. For a detailed choice, see Gitea vs Forgejo vs GitLab.
Why expose SSH on port 2222 instead of 22?
Because the host’s port 22 is already used by the server’s administrative SSH. Exposing the container’s git SSH on a distinct port (2222) avoids any conflict and keeps a clear separation between system access and repository access.
Is Gitea Actions really compatible with GitHub Actions?
Workflow syntax is largely compatible: most simple .yml files work as-is. Differences appear with GitHub Marketplace-specific actions and some advanced features. For standard pipelines (build, test, deploy), migration is usually immediate.
How many resources does Gitea consume?
Very few: the Gitea instance itself runs under 200 MB of RAM at rest, and PostgreSQL adds about 100 to 200 MB. This is one of the main arguments against GitLab, which requires several GB of RAM. An entry-level VPS is sufficient for a small team.
Can I migrate my repositories from GitHub or GitLab?
Yes. Gitea offers an integrated migration feature (New → Migration) that imports not only the code but also issues, pull requests, labels, and milestones from GitHub, GitLab, and other forges. Provide the source repository URL and an access token if necessary.
Should I use PostgreSQL or SQLite?
SQLite is sufficient for strictly personal, low-traffic use and simplifies deployment. As soon as you plan for multiple active users, Actions, or future growth, PostgreSQL (as in this tutorial) offers better concurrent performance and more robust backup capabilities.
Related Topics
- Gitea vs Forgejo vs GitLab: which self-hosted git forge in 2026
- Automatic HTTPS reverse proxy with Caddy and Docker
- Automatic encrypted backup with restic and Backblaze
- Install and secure an Ubuntu VPS from A to Z
With Gitea deployed in Docker behind an HTTPS reverse proxy, you have a complete, fast, and frugal git forge capable of running your CI/CD pipelines without the heaviness of GitLab. It is the ideal tool to take back control of your source code. To follow new versions and vulnerabilities to patch on your self-hosted tools, subscribe to our Telegram watch bot.