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
Arecordgit.example.compointing 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.comand port2222. - 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.