Modern software delivery pipelines increasingly suffer from SaaS vendor lock-in, unpredictable pricing escalations, and latency bottlenecks during high-volume distributed code pushes. While bulky enterprise platforms such as GitLab often demand upwards of 4 GB to 8 GB of RAM merely to initialize basic daemons, developers seeking lean infrastructure on CpanelFree can achieve full GitHub-grade autonomy on a fraction of the compute footprint. Setting up a high-performance self hosted gitea server inside Docker containers decouples data storage, isolates application runtimes, and delivers sub-millisecond Git transactions across enterprise environments.
What is a Self-Hosted Gitea Server and Why Deploy with Docker?
Direct Answer: A self-hosted Gitea server is an ultra-lightweight, open-source Git forge built with Go that delivers comprehensive source control, code review, issue tracking, and CI/CD pipelines at roughly 100 MB of baseline memory. Deploying Gitea via Docker guarantees containerized environment isolation, deterministic configuration management, seamless rolling version updates, and turnkey database segregation using PostgreSQL.
Traditional monolithic source code management platforms introduce complex host dependencies, runtime package conflicts, and fragile upgrade procedures. Gitea circumvents this complexity entirely by distributing a single, compiled Go binary capable of operating as an all-in-one web portal, Git transport listener, and package registry. When wrapped in Docker container primitives, sysadmins gain strict control over resource ceilings, network ingress, and persistent volume lifecycles without polluting the underlying host operating system.
Architectural Blueprint & Topology Overview
In a resilient enterprise deployment, Gitea must never run as a solitary container relying on an embedded SQLite database. High-throughput code checkouts, parallel automated CI/CD webhooks, and multi-developer team environments trigger concurrent file write locks that quickly degrade SQLite databases into catastrophic contention states.
Our production-grade topology segregates concerns across three specialized container layers linked over an isolated internal bridge network:
- Application Layer (Gitea Engine): Executes the rootless Gitea container, serving the responsive web UI, managing authentication, parsing webhooks, and orchestrating Git operations.
- Relational Persistence Layer (PostgreSQL 16): Handles indexed queries, relational metadata, pull request tracking, and branch authorization states with optimized connection pooling.
- Caching & Worker Queue Layer (Redis 7): Absorbs short-lived session states, offloads avatar/attachment memory caches, and queues background mail and webhook workers to maintain instant HTTP response times.
- Ingress & TLS Termination (Nginx Reverse Proxy): Terminates HTTPS certificates, applies rate limiting, accelerates HTTP/2 multiplexing, and forwards raw Git SSH payloads seamlessly.
Architecture Note: Always enforce a dedicated non-root system user (e.g., UID 1000, GID 1000 named
git) on the host machine matching the container user. This prevents permission collisions on bind-mounted directories and stops container-breakout vectors by ensuring the container runtime never writes host files with root privileges.
Host Preparation & Linux Kernel Optimization
Before launching container runtimes, the underlying Linux kernel must be tuned to process high-concurrency Git operations. Heavy repository synchronizations and concurrent clone bursts can exhaust socket buffers and ephemeral port allocations if default Linux kernel limits are left unchanged.
Create a dedicated sysctl configuration file at /etc/sysctl.d/99-gitea.conf to apply optimized network buffers, enhanced backlog queues, and virtual memory parameters:
# /etc/sysctl.d/99-gitea.conf
# Production Linux Kernel Tuning for High-Concurrency Gitea Servers
# Expand system-wide open file descriptor limits
fs.file-max = 2097152
# Enlarge socket listener queue for bursty Git clone and push operations
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 8192
# Expand local ephemeral port range for webhooks and database calls
net.ipv4.ip_local_port_range = 10240 65535
# Allocate larger TCP socket memory buffers for multi-megabyte packfile streaming
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# Implement modern BBR congestion control and Fair Queuing
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
# Protect physical RAM by lowering swap aggressiveness
vm.swappiness = 10
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
Apply these modifications immediately without rebooting by executing:
sudo sysctl --system
Next, configure security limits for your deployment user in /etc/security/limits.d/99-gitea.conf to prevent process exhaustion under load:
# /etc/security/limits.d/99-gitea.conf
git soft nofile 65535
git hard nofile 65535
git soft nproc 32768
git hard nproc 32768
Directory Scaffolding and Permission Isolation
Create the persistent host directory tree under /srv/gitea. Isolate storage permissions strictly to the non-root git user (UID 1000, GID 1000):
# Initialize system user and dedicated service directories
sudo useradd -u 1000 -U -m -s /bin/bash git || true
sudo mkdir -p /srv/gitea/{data,config,postgres,redis}
sudo chown -R 1000:1000 /srv/gitea/data /srv/gitea/config
sudo chmod -R 750 /srv/gitea
Production Docker Compose Specification
The following docker-compose.yml file orchestrates the Gitea container, PostgreSQL database engine, and Redis cache. It enforces strict container health checks, automated restart policies, static logging retention caps, and resource boundaries to prevent noisy-neighbor memory exhaustion.
# /srv/gitea/docker-compose.yml
version: '3.8'
networks:
gitea_internal:
driver: bridge
services:
db:
image: postgres:16-alpine
container_name: gitea_db
restart: always
environment:
POSTGRES_USER: gitea
POSTGRES_PASSWORD: ${DB_PASSWORD:-SuperSecureProductionSecretPassword_2026}
POSTGRES_DB: gitea
volumes:
- /srv/gitea/postgres:/var/lib/postgresql/data
networks:
- gitea_internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitea -d gitea"]
interval: 10s
timeout: 5s
retries: 5
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "3"
deploy:
resources:
limits:
cpus: '2.00'
memory: 2048M
cache:
image: redis:7-alpine
container_name: gitea_cache
restart: always
command: redis-server --requirepass ${REDIS_PASSWORD:-SuperSecureRedisSecretToken_2026} --maxmemory 512mb --maxmemory-policy volatile-lru
volumes:
- /srv/gitea/redis:/data
networks:
- gitea_internal
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-SuperSecureRedisSecretToken_2026}", "ping"]
interval: 10s
timeout: 5s
retries: 3
logging:
driver: "json-file"
options:
max-size: "20m"
max-file: "2"
deploy:
resources:
limits:
cpus: '1.00'
memory: 768M
server:
image: gitea/gitea:1.22-rootless
container_name: gitea_app
restart: always
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
networks:
- gitea_internal
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=db:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=${DB_PASSWORD:-SuperSecureProductionSecretPassword_2026}
volumes:
- /srv/gitea/data:/var/lib/gitea
- /srv/gitea/config:/etc/gitea
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:3000:3000"
- "127.0.0.1:2222:2222"
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
deploy:
resources:
limits:
cpus: '2.00'
memory: 2048M
Standardizing Port 22 SSH Passthrough
By default, running Gitea inside Docker forces Git SSH traffic onto an awkward secondary port, such as 2222. This impairs developer experience, requiring team members to configure custom SSH ports in remote clone URLs (e.g. ssh://[email protected]:2222/org/repo.git) or populate local ~/.ssh/config files.
To deliver an enterprise developer experience, configure the host operating system’s OpenSSH daemon to route incoming git@ connections directly into Gitea on standard port 22, while retaining regular port 22 access for systems administrators.
Create a dedicated routing wrapper script at /usr/local/bin/gitea-shell:
#!/bin/sh
# /usr/local/bin/gitea-shell
# Transparent Host-to-Container SSH Routing Proxy for Gitea
ssh -q \
-p 2222 \
-i /home/git/.ssh/id_rsa \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
[email protected] \
"SSH_ORIGINAL_COMMAND=\"$SSH_ORIGINAL_COMMAND\" $0 $@"
Set appropriate execution and ownership flags on the wrapper script:
sudo chmod +x /usr/local/bin/gitea-shell
sudo chown root:root /usr/local/bin/gitea-shell
Next, configure host OpenSSH in /etc/ssh/sshd_config.d/gitea.conf to enforce shell containment for the git user:
# /etc/ssh/sshd_config.d/gitea.conf
Match User git
AuthorizedKeysFile /srv/gitea/data/git/.ssh/authorized_keys
ForceCommand /usr/local/bin/gitea-shell
PasswordAuthentication no
PubkeyAuthentication yes
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
Reload host SSH daemon to activate standard port 22 Git access: sudo systemctl reload sshd.
Security Hardening: Never mount the host Docker socket (
/var/run/docker.sock) directly into the Gitea container unless you are deploying Gitea Actions runners in rootless DinD (Docker-in-Docker) mode. Giving any web application direct access to the Docker socket allows complete root escalation over the host node.
Hardening the Production app.ini Configuration
Gitea maintains its persistent operational parameters inside custom/conf/app.ini. The following production configuration enforces Redis session persistence, optimizes PostgreSQL connection pooling, establishes strict repository quota boundaries, and locks down installer attack surfaces.
; /srv/gitea/config/conf/app.ini
; High-Throughput Production Settings for Gitea
APP_NAME = Enterprise Gitea Git Forge
RUN_USER = git
WORK_PATH = /var/lib/gitea
RUN_MODE = prod
[server]
PROTOCOL = http
DOMAIN = git.example.com
ROOT_URL = https://git.example.com/
HTTP_ADDR = 0.0.0.0
HTTP_PORT = 3000
DISABLE_SSH = false
SSH_PORT = 22
SSH_LISTEN_PORT = 2222
LFS_START_SERVER = true
LFS_MAX_FILE_SIZE = 10485760000 ; 10GB LFS file ceiling
OFFLINE_MODE = false
[database]
DB_TYPE = postgres
HOST = db:5432
NAME = gitea
USER = gitea
PASSWD = SuperSecureProductionSecretPassword_2026
SCHEMA = public
SSL_MODE = disable
CHARSET = utf8
MAX_OPEN_CONNS = 50
MAX_IDLE_CONNS = 15
CONN_MAX_LIFETIME = 15m
[cache]
ADAPTER = redis
HOST = network=tcp,addr=cache:6379,password=SuperSecureRedisSecretToken_2026,db=0
[queue]
TYPE = redis
CONN_STR = network=tcp,addr=cache:6379,password=SuperSecureRedisSecretToken_2026,db=1
[session]
PROVIDER = redis
PROVIDER_CONFIG = network=tcp,addr=cache:6379,password=SuperSecureRedisSecretToken_2026,db=2
[security]
INSTALL_LOCK = true
SECRET_KEY = 5cf8d743a4e69b22a01948d3c907131e5f884210
REVERSE_PROXY_LIMIT = 1
REVERSE_PROXY_TRUSTED_PROXIES = 127.0.0.1,172.16.0.0/12
DISABLE_GIT_HOOKS = true
[repository]
MAX_CREATION_LIMIT = 500
ENABLE_PUSH_CREATE_USER = true
ENABLE_PUSH_CREATE_ORG = true
DEFAULT_BRANCH = main
TLS Termination & Reverse Proxy Configuration (Nginx)
Placing Gitea behind an Nginx reverse proxy ensures secure TLS 1.3 termination, enables HTTP/2 multiplexing, and accommodates massive Git LFS packfile uploads without request timeouts.
# /etc/nginx/conf.d/gitea.conf
upstream gitea_backend {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 80;
listen [::]:80;
server_name git.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name git.example.com;
ssl_certificate /etc/letsencrypt/live/git.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/git.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Accommodate large Git commits, binary packages, and LFS chunks
client_max_body_size 512M;
# Extended proxy timeouts for prolonged packfile generation
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
location / {
proxy_pass http://gitea_backend;
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_http_version 1.1;
proxy_set_header Connection "";
}
}
Performance Benchmarks: Default vs. Tuned Production Topology
Benchmarking an untuned Gitea container against our tuned three-tier architecture reveals dramatic latency improvements and concurrency ceilings under high-load synthetic Git operations.
| Feature / Metric | Standard / Default | Tuned / Production |
|---|---|---|
| Database Backend | SQLite3 (File Lock Bottleneck) | PostgreSQL 16 (Connection Pool) |
| Idle Memory Footprint | ~250 MB (Unconstrained) | ~110 MB (Go GC Tuned) |
| SSH Architecture | Container Port 2222 (Non-standard) | Host Port 22 Passthrough (AuthorizedKeys) |
| Concurrent Git Clones | 10-15 Concurrency Limit | 200+ Requests / sec via NVMe |
| Backup & Recovery RPO | Manual tarball dumps | Automated WAL-G / S3 Sync (15m RPO) |
Automated Disaster Recovery and Snapshot Script
A reliable self-hosting architecture requires zero-downtime, consistent backup procedures. Because Gitea relies on relational database records alongside raw Git repository directories on the filesystem, executing simple file copies while the database is actively writing risks corrupted, inconsistent backups.
Deploy the following automated backup script at /usr/local/bin/backup-gitea.sh to create synchronized, atomic snapshots:
#!/usr/bin/env bash
# /usr/local/bin/backup-gitea.sh
# Production Automated Backup Automation for Gitea + PostgreSQL Stack
set -euo pipefail
BACKUP_DIR="/var/backups/gitea"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
RETENTION_DAYS=14
mkdir -p "${BACKUP_DIR}"
echo "[$(date)] Starting Gitea internal archive generation..."
docker exec -u git gitea_app gitea dump -c /etc/gitea/app.ini -f /tmp/gitea-dump-${TIMESTAMP}.zip
echo "[$(date)] Copying archive to local backup storage..."
docker cp gitea_app:/tmp/gitea-dump-${TIMESTAMP}.zip "${BACKUP_DIR}/"
docker exec -u git gitea_app rm -f /tmp/gitea-dump-${TIMESTAMP}.zip
echo "[$(date)] Executing PostgreSQL relational database dump..."
docker exec gitea_db pg_dump -U gitea -d gitea -F c -b -v -f /tmp/gitea_db_${TIMESTAMP}.dump
docker cp gitea_db:/tmp/gitea_db_${TIMESTAMP}.dump "${BACKUP_DIR}/"
docker exec gitea_db rm -f /tmp/gitea_db_${TIMESTAMP}.dump
echo "[$(date)] Pruning backups older than ${RETENTION_DAYS} days..."
find "${BACKUP_DIR}" -name "gitea*" -type f -mtime +${RETENTION_DAYS} -delete
echo "[$(date)] Gitea backup cycle completed successfully."
Schedule this script in root crontab to execute every night at 02:00 AM: 0 2 * * * /usr/local/bin/backup-gitea.sh >> /var/log/gitea-backup.log 2>&1.
Scaling Infrastructure & Storage I/O Optimization
When scaling beyond a handful of private developers into high-throughput continuous deployment environments, disk I/O latency becomes the primary bottleneck. Standard virtual servers with shared mechanical disks or throttled network-attached storage experience severe I/O stalls during repository garbage collection and concurrent Git operations. For mission-critical production instances where performance consistency and cost predictability are paramount, deploying on MeraHost Enterprise Cloud provides dedicated enterprise NVMe storage arrays, LiteSpeed optimization, and a transparent pricing policy with zero renewal price hikes.
Frequently Asked Questions
How much RAM and CPU does a self-hosted Gitea server require in production?
In contrast to resource-heavy alternatives like GitLab, a base Gitea container runs efficiently on 512 MB of RAM and a single vCPU core for small development teams (10-25 developers). For enterprise teams with 100+ active contributors running continuous CI/CD pipelines and large Git LFS operations, allocating 2 vCPUs, 2 GB to 4 GB of RAM, and high-IOPS NVMe storage ensures sub-second response times.
What is the difference between Gitea and Forgejo?
Forgejo is a hard community fork of Gitea initiated in late 2022 to preserve a strictly community-governed, non-commercial software model. Both platforms maintain substantial architectural and API compatibility, running on the identical Go foundation. The Docker deployment configurations, PostgreSQL integrations, and SSH passthrough scripts outlined in this guide operate identically across both Gitea and Forgejo images.
Why should I choose PostgreSQL over SQLite for Gitea in Docker?
SQLite uses file-level locking mechanisms. When multiple developers push code simultaneously or automated CI systems poll webhooks, SQLite locks the database file, creating serialization queues and timeout errors. PostgreSQL 16 supports concurrent row-level locking, robust connection pooling, point-in-time recovery (PITR), and reliable atomic backups without halting Gitea service operations.
How do I configure Git LFS (Large File Storage) with self-hosted Gitea?
Gitea includes a built-in Git LFS server enabled via LFS_START_SERVER = true under the [server] block in app.ini. Large binary files are stored in /var/lib/gitea/data/lfs by default, or they can be routed directly to S3-compatible object storage by configuring the [lfs] section with bucket credentials, preventing disk bloat on your primary server.
Deploy Enterprise-Grade Production Infrastructure
Need guaranteed performance with zero price hikes? Host mission-critical workloads on MeraHost with pure Enterprise NVMe, LiteSpeed Web Server, and Same Renewal Price, Always (starting at ₹99/mo).
