🛠️ Tutorials · ⏱ 9 min read

Gitea with Docker in 2026: Deploy a Self-Hosted Git Forge with HTTPS

2026 tutorial on deploying Gitea, the lightweight self-hosted git forge, using Docker. Covers PostgreSQL, HTTPS reverse proxy, dedicated SSH port, CI/CD Actions, hardening, and backups. Ready-to-use docker-compose and Caddy configs included.

S By Selfhostr Team · independent tests
Gitea with Docker in 2026: Deploy a Self-Hosted Git Forge with HTTPS
ⓘ This article may contain affiliate links (no extra cost to you, it supports our tests). See the disclosure.
💾
2 GB
Min. RAM
⚙️
1 vCPU
Min. CPU
Seconds
Startup Time
🔄
Gitea Actions
CI/CD Engine

👍 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

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:

TypeNameValue
Agit.exemple.fr203.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 editing app.ini.
  • ROOT_URL is 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 internal network isolates PostgreSQL; only Gitea joins web to 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.sock gives 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 localhost or the wrong port. ROOT_URL, SSH_DOMAIN, or SSH_PORT are 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 shared web network and ensure the container is running (docker logs gitea).
  • Gitea cannot connect to the database. The DB_PASS differs between the two services, or PostgreSQL hasn’t finished starting. Check docker 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.

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.

Tags: GiteaDockerGitSelf-hostedCI/CDCaddyPostgreSQL

Related

🛠️ Tutorials

Deploy n8n with Docker and HTTPS in 2026: Complete Tutorial (Postgres, Reverse Proxy, Persistence)

Step-by-step 2026 guide to self-host n8n, the no-code automation platform, using Docker Compose. Features PostgreSQL, workflow persistence, automatic HTTPS via Caddy and Let's Encrypt, credential encryption, backups, and hardening. Ready-to-use configs.

Read
🛠️ Tutorials

Nextcloud All-in-One Docker Tutorial 2026: Full Setup & Auto HTTPS

2026 guide to deploying Nextcloud All-in-One with Docker. Covers master container, HTTPS reverse proxy, backups, hardening, and troubleshooting. Includes ready-to-use docker-compose and Caddy configs for your personal cloud.

Read
⚖️ Comparisons

Gitea vs Forgejo vs GitLab Self-Hosted 2026: Which Git Forge to Choose

Technical comparison of Gitea, Forgejo, and GitLab CE in 2026. Analyze RAM benchmarks, CI/CD capabilities, governance models, and use cases to select the best self-hosted Git forge.

Read