Self-hosting Git becomes a strategic platform decision once compliance, uptime, and developer velocity start pulling in different directions. This guide shows a production-ready deployment of Gitea on Ubuntu using Docker Compose, Traefik, and PostgreSQL.
The intent is reliability under realistic conditions: safe secret boundaries, predictable TLS behavior, clean rollback points, and validation steps that reduce surprises during incident response windows.
Use this runbook when your team needs an opinionated baseline that is easy to operate, straightforward to audit, and simple to hand over between platform and engineering teams without tribal knowledge.
Architecture/flow overview
Traffic enters Traefik on ports 80 and 443, where HTTP is redirected to HTTPS and certificates are managed automatically. Traefik forwards only the trusted host rule to the Gitea web service. PostgreSQL remains on an internal network and is never exposed publicly.
This separation supports least privilege and clearer fault isolation. If HTTPS fails, you troubleshoot Traefik and DNS. If repository actions fail, you evaluate Gitea application logs and database health. Reduced ambiguity shortens MTTR when production pressure is high.
For team operations, define ownership explicitly: platform owns host security and reverse proxy lifecycle, developer tooling owners handle application configuration and upgrades, and security owners define MFA, token policy, and retention controls.
Operationally, this architecture also improves change management. You can stage proxy configuration changes independently from application upgrades, and you can run controlled database maintenance windows without exposing public services.
Prerequisites
- Ubuntu 22.04 or 24.04 host with at least 2 vCPU and 4 GB RAM.
- DNS A record such as
git.example.compointing to the server. - Docker Engine and Docker Compose plugin installed.
- Open inbound ports 80/443 from the internet.
- A secure location for secrets (password manager or vault).
Before deployment, align standards for repository defaults, branch protection, and admin onboarding. Strong governance decisions made early save major cleanup effort when repository count grows quickly.
Define minimum compliance controls now: MFA policy, access review frequency, secret rotation cadence, and incident ownership. Teams that codify these requirements up front avoid risky ad-hoc behavior under delivery pressure.
sudo apt-get update
sudo apt-get install -y docker.io docker-compose-plugin curl
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker
docker --version
docker compose versionIf the Copy button does not work in your editor/browser, manually copy commands from the block.
Step-by-step deployment
Create a controlled project structure under /opt/gitea so service state, proxy config, and backup assets are isolated and predictable:
sudo mkdir -p /opt/gitea/{traefik,gitea,postgres,backups}
sudo chown -R $USER:$USER /opt/gitea
chmod 700 /opt/gitea/backups
cd /opt/giteaIf the Copy button does not work in your editor/browser, manually copy commands from the block.
Create an environment file and lock permissions. Keep this file out of version control and broad infrastructure snapshots:
cat > /opt/gitea/.env <<'EOF'
DOMAIN=git.example.com
[email protected]
GITEA_DB_NAME=gitea
GITEA_DB_USER=gitea
GITEA_DB_PASSWORD=replace-with-strong-random-secret
POSTGRES_PASSWORD=replace-with-another-strong-secret
GITEA_SECRET_KEY=replace-with-64-char-random-value
GITEA_INTERNAL_TOKEN=replace-with-64-char-random-value
EOF
chmod 600 /opt/gitea/.envIf the Copy button does not work in your editor/browser, manually copy commands from the block.
Deploy the stack with explicit health checks and reverse-proxy labels:
services:
traefik:
image: traefik:v3.0
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.letsencrypt.acme.email=platform@example.com
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge=true
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
ports: ["80:80", "443:443"]
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik:/letsencrypt
restart: unless-stopped
postgres:
image: postgres:16
environment:
POSTGRES_DB: gitea
POSTGRES_USER: gitea
POSTGRES_PASSWORD: replace-with-strong-secret
volumes: ["./postgres:/var/lib/postgresql/data"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitea -d gitea"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
gitea:
image: gitea/gitea:1.22
depends_on:
postgres:
condition: service_healthy
environment:
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: postgres:5432
GITEA__database__NAME: gitea
GITEA__database__USER: gitea
GITEA__database__PASSWD: replace-with-strong-secret
GITEA__server__ROOT_URL: https://git.example.com/
GITEA__security__SECRET_KEY: replace-with-64-char-random-value
GITEA__security__INTERNAL_TOKEN: replace-with-64-char-random-value
GITEA__service__DISABLE_REGISTRATION: "true"
labels:
- traefik.enable=true
- traefik.http.routers.gitea.rule=Host(`git.example.com`)
- traefik.http.routers.gitea.entrypoints=websecure
- traefik.http.routers.gitea.tls.certresolver=letsencrypt
- traefik.http.services.gitea.loadbalancer.server.port=3000
volumes: ["./gitea:/data"]
restart: unless-stoppedIf the Copy button does not work in your editor/browser, manually copy commands from the block.
cd /opt/gitea
docker compose --env-file .env up -d
docker compose ps
docker compose logs --tail=150 giteaIf the Copy button does not work in your editor/browser, manually copy commands from the block.
At first login, complete admin setup, disable unnecessary open registration, and set repository policy defaults. Document these choices as a baseline so future admins donβt reintroduce weak settings.
For enterprise environments, define service account boundaries and token scopes early. This avoids accidental use of privileged personal tokens in CI pipelines, which is a common root cause in audit findings.
Capture a baseline configuration snapshot after launch, including enabled auth settings, default repository policies, webhook constraints, and backup schedule. This snapshot becomes your reference for drift detection later.
Configuration and secrets handling
Keep secrets and long-lived tokens in a dedicated manager, then inject them at deployment runtime. Rotate credentials after staffing changes, incident response events, or policy updates.
Enable 2FA for administrators, require strong password policy, and enforce branch protection for default branches. Guardrails at the platform level remove burden from each repository owner.
Integrate logs and metrics from Traefik, Gitea, and PostgreSQL into a central system. A single pane of correlation across proxy, app, and database events dramatically reduces troubleshooting time during production incidents.
Also define explicit retention and access controls for logs containing authentication or repository metadata. Keeping observability data secure is as important as collecting it.
cat > /opt/gitea/backups/backup-gitea.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd /opt/gitea
source .env
docker compose exec -T postgres pg_dump -U "$GITEA_DB_USER" "$GITEA_DB_NAME" > backups/db-backup.sql
tar -czf backups/gitea-data-backup.tgz gitea
echo "apply retention policy with your preferred scheduler"
EOF
chmod +x /opt/gitea/backups/backup-gitea.sh
/opt/gitea/backups/backup-gitea.shIf the Copy button does not work in your editor/browser, manually copy commands from the block.
Verification
Do not declare success based on container status alone. Validate end-user flows and operational controls:
- HTTPS certificate chain is valid and not near expiration.
- Repository create/push/clone works from a clean client.
- Webhook delivery succeeds to a test endpoint.
- Backup and restore runbook is tested in a separate environment.
curl -I https://git.example.com
openssl s_client -connect git.example.com:443 -servername git.example.com </dev/null 2>/dev/null | openssl x509 -noout -dates
cd /opt/gitea && docker compose ps
cd /opt/gitea && docker compose exec -T postgres pg_isready -U "$GITEA_DB_USER" -d "$GITEA_DB_NAME"If the Copy button does not work in your editor/browser, manually copy commands from the block.
Capture these outputs in change records to preserve objective evidence. During future incidents, these known-good snapshots provide immediate context and reduce guesswork.
For production readiness reviews, include verification artifacts for TLS, application health, backup success, and one full restore test. A single checklist with evidence is far more durable than informal approvals.
Common issues and fixes
Certificates are not issued or renewed
Verify public DNS points to the correct host, check inbound firewall rules, and confirm ACME challenge traffic can reach Traefik. Watch logs for rate-limit responses and avoid repeated blind retries.
Gitea is reachable but repository actions fail
Check PostgreSQL health, credentials, and disk capacity first. Most early failures are database connectivity or resource exhaustion, not application logic defects.
Slow clone/push performance during peak hours
Inspect CPU saturation and disk IOPS, then optimize host resources or move storage to faster disks. Also verify reverse-proxy timeout settings for large repository traffic.
Backups run but recovery is incomplete
Ensure your restore process includes both database and Gitea data directory. Recovery readiness is measured by successful restore tests, not by backup file presence.
Across all failure types, maintain an incident template that records timeline, blast radius, root cause, and permanent corrective action. This improves reliability with each event.
Backups and lifecycle operations
Adopt a lifecycle calendar: monthly patching, quarterly credential rotation, quarterly restore drills, and staged application upgrades. Tie each operation to explicit owners and approval gates.
Track growth indicators such as repository count, active users, storage trajectory, and webhook load. Capacity planning based on trend data prevents emergency scaling and service instability.
When introducing SSO or external CI integrations, pilot changes in staging and document rollback criteria. Clear rollback rules reduce stress and protect developer productivity during maintenance windows.
Finally, schedule recurring architecture reviews. As teams expand, requirements around auditability, residency, and high availability evolve; periodic reviews keep your deployment aligned with business risk tolerance.
FAQ
Is PostgreSQL required for production?
It is the recommended default for reliability, backup tooling, and concurrency. SQLite is fine for tiny lab setups but not ideal for sustained multi-user operations.
Can I use this with private runners and CI pipelines?
Yes. Use scoped service tokens, rotate credentials periodically, and enforce least privilege on organizations and repositories used by automation.
Should SSH Git access be enabled?
Enable it only if your workflow requires it. If enabled, harden host SSH, document port usage, and monitor authentication attempts.
How often should I test restore procedures?
At least monthly for critical teams, and after any backup script changes. Restore confidence decays quickly without regular drills.
What metrics should trigger urgent action?
Rising 5xx rates, failed webhook deliveries, certificate expiry horizon, database health failures, and backup job failures are top priority indicators.
Can I migrate from another Git platform later?
Yes. Plan repository import sequencing, identity mapping, webhook migrations, and freeze windows to minimize disruption.
Internal links
- Related: production deployment pattern in Guides
- Related: hardening and verification playbook
- Related: troubleshooting style used across Guides
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.