Skip to content
+1 (813) 212-3723 support@layeronecloud.com
Self-hosting VPS guides

How to host Gitea on a VPS with rootless Docker and HTTPS

Run Gitea with its rootless container, SQLite, Nginx HTTPS, SSH clones, closed registration, and consistent off-server backups.

6 min read

Gitea is a lightweight Git forge with repositories, issues, pull requests, packages, releases, and Actions integration. A small team can run it with the official rootless image and SQLite, while larger or write-heavy installations should plan PostgreSQL and more memory separately.

This guide keeps Gitea's web port on loopback behind Nginx. It publishes only a dedicated Git SSH port, 2222, so the host's administrative SSH can stay on 22. The image's 1-rootless tag follows stable Gitea 1.x releases without tracking nightly builds.

VPS size, CPU, storage, and price

Item Planning value
Minimum RAM 1 GB for a very small rootless Gitea + SQLite deployment
Recommended RAM 2 GB for the OS, Docker, Git operations, indexing, and update headroom
Expected CPU usage About 0.05–0.20 vCPU idle; up to 1 vCPU during clones, pushes, archive creation, indexing, or package operations
Storage requirement Reserve 10 GB for Ubuntu, Docker, and Gitea, then add Git objects, LFS, packages, attachments, releases, Actions artifacts, logs, and backup working space
Exact LayerOne SKU gc.micro — slug layerone-starter, 1 vCPU, 2 GB RAM, 40 GB disk
Expected monthly cost $0.0068 per active hour; about $4.96 per 730 hours against the advertised $5.00 730-hour equivalent

A 744-hour month is about $0.0068 × 744 = $5.06. Repository compression, LFS, packages, Actions artifacts, transfer, and storage growth vary. LayerOne bills the allocated VPS hourly, and CPU figures are planning estimates rather than Git throughput guarantees. Gitea Actions runners execute arbitrary job code and need separate sizing and security boundaries; they are not included in these numbers.

Assumptions

  • Ubuntu 22.04 or 24.04 with Docker Engine and Compose; complete Running Docker first
  • DNS A record git.example.com pointing to the public LayerOne IPv4
  • A sudo user with SSH keys and a fixed source address called ADMIN_IP
  • A small private team for which SQLite's simple single-file operation is an intentional choice

1. Define the firewall policy

In the LayerOne cloud firewall, use inbound default DROP and allow:

Source Protocol Port Purpose
ADMIN_IP/32 TCP 22 Host administration
0.0.0.0/0 TCP 80 ACME validation and HTTPS redirect
0.0.0.0/0 TCP 443 Gitea web and HTTPS Git
Trusted users, or 0.0.0.0/0 TCP 2222 Git over SSH

Restrict 2222 to your team/VPN if every contributor has predictable addresses. Do not expose Gitea's port 3000.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from ADMIN_IP to any port 22 proto tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 2222/tcp
sudo ufw enable
sudo ufw status verbose

Docker can bypass UFW for published ports. Port 3000 is therefore bound to loopback in Compose, and the LayerOne cloud firewall remains the outer control for 2222.

2. Create the rootless Gitea project

sudo install -d -m 0750 /opt/gitea/data /opt/gitea/config
sudo chown "$USER":"$USER" /opt/gitea
sudo chown -R 1000:1000 /opt/gitea/data /opt/gitea/config
cd /opt/gitea

The official rootless image runs as UID/GID 1000 by default, so those ownership values are load-bearing. Create /opt/gitea/compose.yaml:

services:
  gitea:
    image: docker.gitea.com/gitea:1-rootless
    container_name: gitea
    restart: always
    environment:
      GITEA__server__DOMAIN: git.example.com
      GITEA__server__ROOT_URL: https://git.example.com/
      GITEA__server__SSH_DOMAIN: git.example.com
      GITEA__server__SSH_PORT: "2222"
      GITEA__server__START_SSH_SERVER: "true"
      GITEA__service__DISABLE_REGISTRATION: "true"
    volumes:
      - ./data:/var/lib/gitea
      - ./config:/etc/gitea
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "127.0.0.1:3000:3000"
      - "2222:2222"

Validate and start it:

cd /opt/gitea
docker compose config
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 gitea
curl -I http://127.0.0.1:3000

Rootless reduces the container process's host privileges. It does not make a compromised forge harmless: Gitea still holds repositories, tokens, webhooks, package credentials, and user data.

3. Add Nginx HTTPS

sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx

Create /etc/nginx/sites-available/git.example.com:

server {
    listen 80;
    server_name git.example.com;
    client_max_body_size 512m;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        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 Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
sudo ln -s /etc/nginx/sites-available/git.example.com   /etc/nginx/sites-enabled/git.example.com
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d git.example.com
sudo certbot renew --dry-run

Increase client_max_body_size only when a known repository or package needs it; large Git data belongs in normal Git/LFS protocols, not arbitrary web uploads.

4. Complete the installer safely

Open https://git.example.com. For this small-team design:

  • Select SQLite3.
  • Keep the application data and repository paths under /var/lib/gitea.
  • Confirm the site URL is https://git.example.com/.
  • Confirm SSH uses domain git.example.com and port 2222.
  • Expand administrator settings and create the first named administrator before finishing setup.

Registration is disabled by Compose, so add users from Site Administration or through an approved identity provider. Do not temporarily open registration on a public URL and hope to close it before bots arrive.

5. Verify HTTPS, Git HTTPS, and Git SSH

Create a private empty repository called smoke-test, then run from a workstation with your own username:

curl -I https://git.example.com
git clone https://git.example.com/USERNAME/smoke-test.git
git clone ssh://git@git.example.com:2222/USERNAME/smoke-test.git

Verify the SSH host-key fingerprint over a trusted channel before accepting it. Commit and push a small file over both allowed protocols, then check that the web UI displays it. Confirm that an unauthenticated browser cannot see the private repository and cannot self-register.

Security pitfalls

  • Keep registration disabled unless an intentional invitation/identity flow is ready.
  • Require individual accounts, strong credentials, and multi-factor authentication. Store recovery codes outside Gitea.
  • Protect deploy keys, personal access tokens, webhook secrets, package tokens, and OAuth client secrets.
  • Do not run an Actions runner in this same trust boundary merely because Gitea offers Actions. A workflow can execute repository-controlled code; place runners on separate, disposable infrastructure with only the access each job needs.
  • Use Git LFS or an object-storage design for large binaries; normal Git history keeps every version and can fill a small disk quickly.
  • Watch free disk and inode counts. A forge may appear healthy until the next push is the write that exhausts the filesystem.

Consistent backup and restore

Gitea's official backup guidance says to stop the instance for consistency, because its database, repositories, and files change separately. For this bind-mounted rootless + SQLite layout:

cd /opt/gitea
docker compose stop gitea
GITEA_BACKUP="/var/backups/gitea/gitea-$(date +%F).tar.gz"
sudo install -d -m 0700 -o "$USER" -g "$USER" /var/backups/gitea
sudo tar -C /opt -czf "$GITEA_BACKUP"   gitea/data gitea/config gitea/compose.yaml
sudo chown "$USER":"$USER" "$GITEA_BACKUP"
chmod 600 "$GITEA_BACKUP"
docker compose start gitea
docker compose ps
unset GITEA_BACKUP

Transfer the archive to encrypted off-server storage immediately. To test a restore, unpack it on an isolated host, restore ownership to UID/GID 1000, start the same image version, and clone/check representative repositories. LayerOne does not take snapshots or backups; see Backups and snapshots.

For larger PostgreSQL deployments, use a consistent native pg_dump plus the Gitea repository/data directories; a copy of only one side is not restorable.

Upgrade deliberately

Read Gitea's release and breaking-change notes, stop writes, and take the tested backup first:

cd /opt/gitea
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=150 gitea

Then repeat web login, unauthenticated access, HTTPS clone/push, SSH clone/push, webhooks, and package tests. Resolve deprecated app.ini options before the release that removes them.

Official and LayerOne references

Still stuck

Chat with us from the portal.

Ask the assistant from the Chat bar. During business hours you can ask for a person and a human joins live.