Primary keyword: Deploy Gitea with Docker Compose and Caddy
Secondary keywords: self-hosted git server, Gitea reverse proxy, Caddy TLS automation, Ubuntu Docker deployment, PostgreSQL backup strategy
Introduction: what it is and a real-world use case
Gitea is a lightweight open-source Git service that gives teams fast repo hosting, pull requests, and full data ownership. A practical use case is an internal engineering team replacing hosted Git for compliance and cost control.
This guide covers a production-ready pattern: Gitea + PostgreSQL on Docker Compose, fronted by Caddy for automatic HTTPS.
Prerequisites
- Ubuntu 22.04/24.04 server (2 vCPU, 4 GB RAM)
- DNS A record for
git.example.com - SSH user with sudo
- Open ports 22/80/443
Step 1: Install Docker Engine and Compose plugin
sudo apt update
sudo apt -y install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] 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 -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker
Step 2: Prepare directories and secrets
sudo mkdir -p /opt/gitea/{data,postgres,caddy,config}
sudo chown -R $USER:$USER /opt/gitea
cat > /opt/gitea/.env <<'EOF'
GITEA_DOMAIN=git.example.com
POSTGRES_DB=gitea
POSTGRES_USER=gitea
POSTGRES_PASSWORD=CHANGE_ME_TO_LONG_RANDOM_PASSWORD
GITEA_SECRET_KEY=CHANGE_ME_TO_64_CHAR_SECRET
GITEA_INTERNAL_TOKEN=CHANGE_ME_TO_64_CHAR_TOKEN
EOF
chmod 600 /opt/gitea/.env
Step 3: Create docker-compose.yml
services:
postgres:
image: postgres:16-alpine
container_name: gitea-postgres
restart: unless-stopped
env_file: .env
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
interval: 10s
timeout: 5s
retries: 6
gitea:
image: gitea/gitea:1.22.1
container_name: gitea
restart: unless-stopped
env_file: .env
depends_on:
postgres:
condition: service_healthy
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: postgres:5432
GITEA__database__NAME: ${POSTGRES_DB}
GITEA__database__USER: ${POSTGRES_USER}
GITEA__database__PASSWD: ${POSTGRES_PASSWORD}
GITEA__server__DOMAIN: ${GITEA_DOMAIN}
GITEA__server__ROOT_URL: https://${GITEA_DOMAIN}/
GITEA__security__INSTALL_LOCK: "true"
GITEA__security__SECRET_KEY: ${GITEA_SECRET_KEY}
GITEA__security__INTERNAL_TOKEN: ${GITEA_INTERNAL_TOKEN}
GITEA__service__DISABLE_REGISTRATION: "true"
volumes:
- ./data:/data
expose:
- "3000"
- "22"
caddy:
image: caddy:2.8
container_name: gitea-caddy
restart: unless-stopped
depends_on:
- gitea
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./config/caddy_data:/data
- ./config/caddy_config:/config
Step 4: Add Caddy reverse proxy config
cat > /opt/gitea/caddy/Caddyfile <<'EOF'
{$GITEA_DOMAIN} {
encode zstd gzip
reverse_proxy gitea:3000
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
EOF
Step 5: Start services and validate
cd /opt/gitea
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=80 gitea
docker compose logs --tail=80 caddy
Configuration and security best practices
- Rotate DB and app secrets on a schedule.
- Disable open registration and enforce SSO where possible.
- Pin image tags and patch monthly.
- Back up /opt/gitea/data and PostgreSQL daily.
- Restrict SSH source IPs and enable MFA for admins.
Alternative deployment options
- Kubernetes + Helm for multi-node scaling.
- Docker Compose + Traefik for existing Traefik-based stacks.
- Systemd + native binaries when minimizing container layers.
Verification steps
curl -I https://git.example.com
curl -s https://git.example.com/api/v1/version
docker compose ps
docker exec -it gitea-postgres psql -U gitea -d gitea -c '\dt'
Common issues and fixes
502 Bad Gateway
Ensure Caddy and Gitea are on the same Docker network and proxying to gitea:3000.
Container restart loop
Usually DB credentials mismatch; verify .env and container logs.
TLS certificate not issued
Check DNS propagation and open 80/443 to the public internet.
Slow clone performance
Check disk IOPS and move PostgreSQL data to faster storage.
FAQ
Can I use SQLite first?
Yes for tests; production should use PostgreSQL.
How should backups be done?
Daily dumps + encrypted offsite copies + restore drills.
How do I upgrade safely?
Snapshot, pull pinned updates, restart, then verify critical flows.
Is Caddy required?
No. Nginx and Traefik are valid alternatives.
What server size is a good start?
2 vCPU and 4–8 GB RAM is a practical baseline for small teams.