Skip to Content

Deploy Gitea with Docker Compose and Caddy on Ubuntu (Production Guide)

Hands-on production deployment with security, verification, and operations checklist.

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.

Internal linking suggestions

Deploy Uptime Kuma with Docker Compose and Caddy on Ubuntu (Production Guide)
A practical, secure setup for teams who need reliable uptime alerts without SaaS lock-in.