Skip to Content

Production Guide: Deploy Gitea with Docker Swarm + Traefik + PostgreSQL on Ubuntu

A practical production blueprint for reliable self-hosted Git with secure ingress, secrets, backups, and operational checks.

Many teams start with a single-container Git server and only think about production architecture after the first serious outage. The pattern usually repeats: repositories become central to every workflow, CI jobs multiply, and suddenly even a short service interruption blocks engineering work across multiple teams. This guide is designed for that exact transition point. You already know why self-hosted Git matters for cost, sovereignty, and controlβ€”you now need a deployment model that is resilient, secure, and maintainable.

We will deploy Gitea on Ubuntu using Docker Swarm, Traefik, and PostgreSQL. This stack gives you practical reliability without the operational overhead of a full Kubernetes platform. Swarm provides service orchestration and runtime secrets, Traefik handles HTTPS and routing automatically, and PostgreSQL gives durable transactional storage suitable for production usage.

The objective is not a demo. We focus on production behavior: clear service boundaries, minimal exposed surface area, secret rotation readiness, backup discipline, and post-change verification. By the end, you should have a repeatable blueprint that can serve a growing engineering organization and survive real incidents with predictable recovery steps.

Architecture and flow overview

The runtime has three core services connected through a private overlay network:

  • Traefik terminates TLS, enforces HTTPS entrypoints, and routes hostnames to internal services.
  • Gitea provides repository hosting, pull requests, issues, webhooks, and API access.
  • PostgreSQL stores metadata, users, auth state, and project records with strong consistency.

External traffic arrives at git.yourdomain.com. Traefik forwards only matching traffic to the Gitea service. PostgreSQL stays unexposed to the public network. This separation is operationally important: routing incidents, database incidents, and application incidents can be diagnosed and remediated independently.

Swarm secrets provide a cleaner and safer alternative to plain environment variables for highly sensitive values. Even if your team is small, adopting this pattern early avoids brittle migration work later.

Prerequisites

  • Ubuntu 22.04/24.04 host with at least 2 vCPU, 4 GB RAM, and SSD storage.
  • DNS records for git.yourdomain.com pointing to the server IP.
  • Ports 80 and 443 open in your cloud firewall/security group.
  • Sudo access over SSH.
  • Decision on where off-host backups will be copied (object storage or backup node).
sudo apt update && sudo apt -y upgrade
sudo apt -y install ca-certificates curl gnupg ufw apache2-utils
sudo mkdir -m 0755 -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /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

If the copy button does not work in your browser/editor, manually select the command block and copy it.

Step-by-step deployment

1) Bootstrap Swarm runtime and create secrets

Create the project paths, initialize Swarm, create the shared overlay network, and generate cryptographically strong secrets for database and internal tokens.

mkdir -p ~/gitea-swarm/{traefik,gitea,postgres,backups}
cd ~/gitea-swarm
docker swarm init --advertise-addr $(hostname -I | awk '{print $1}')
docker network create --driver overlay --attachable edge
openssl rand -base64 48 | tr -d '
' | docker secret create gitea_db_password -
openssl rand -base64 48 | tr -d '
' | docker secret create gitea_internal_token -
openssl rand -base64 48 | tr -d '
' | docker secret create gitea_secret_key -

If the copy button does not work in your browser/editor, manually select the command block and copy it.

2) Define and deploy Traefik, PostgreSQL, and Gitea

Use a single Swarm stack file so your deployment remains declarative and version-controlled. Keep data volumes on persistent storage and never expose PostgreSQL directly.

cat > stack.yml << 'EOF'
version: "3.9"
services:
  traefik:
    image: traefik:v3.0
    command:
      - --providers.docker.swarmMode=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.le.acme.tlschallenge=true
      - [email protected]
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
    ports: ["80:80", "443:443"]
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik:/letsencrypt
    networks: [edge]
    deploy:
      placement:
        constraints: [node.role == manager]

  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: gitea
      POSTGRES_USER: gitea
      POSTGRES_PASSWORD_FILE: /run/secrets/gitea_db_password
    secrets: [gitea_db_password]
    volumes: ["./postgres:/var/lib/postgresql/data"]
    networks: [edge]

  gitea:
    image: gitea/gitea:1.22
    environment:
      GITEA__server__ROOT_URL: https://git.yourdomain.com/
      GITEA__database__DB_TYPE: postgres
      GITEA__database__HOST: postgres:5432
      GITEA__database__NAME: gitea
      GITEA__database__USER: gitea
      GITEA__database__PASSWD__FILE: /run/secrets/gitea_db_password
      GITEA__security__INTERNAL_TOKEN__FILE: /run/secrets/gitea_internal_token
      GITEA__security__SECRET_KEY__FILE: /run/secrets/gitea_secret_key
      GITEA__service__DISABLE_REGISTRATION: "true"
    secrets: [gitea_db_password,gitea_internal_token,gitea_secret_key]
    volumes: ["./gitea:/data"]
    networks: [edge]
    deploy:
      labels:
        - traefik.enable=true
        - traefik.docker.network=edge
        - traefik.http.routers.gitea.rule=Host(`git.yourdomain.com`)
        - traefik.http.routers.gitea.entrypoints=websecure
        - traefik.http.routers.gitea.tls.certresolver=le
        - traefik.http.services.gitea.loadbalancer.server.port=3000
networks:
  edge: {external: true}
secrets:
  gitea_db_password: {external: true}
  gitea_internal_token: {external: true}
  gitea_secret_key: {external: true}
EOF

If the copy button does not work in your browser/editor, manually select the command block and copy it.

docker stack deploy -c stack.yml scm
docker stack services scm
docker stack ps scm --no-trunc
curl -I https://git.yourdomain.com

If the copy button does not work in your browser/editor, manually select the command block and copy it.

3) Configure backup automation

For Git platforms, a backup policy must include both application data and database state. The script below creates a compressed DB dump and data archive, then prunes old snapshots.

cat > ~/gitea-swarm/backup-gitea.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd ~/gitea-swarm
TS=$(date +%F-%H%M%S)
CID=$(docker ps --filter name=scm_postgres -q | head -n1)
docker exec "$CID" sh -lc 'export PGPASSWORD=$(cat /run/secrets/gitea_db_password); pg_dump -U gitea -d gitea -Fc' > "./backups/gitea-${TS}.dump"
tar -czf "./backups/gitea-data-${TS}.tgz" ./gitea
find ./backups -type f -mtime +14 -delete
EOF
chmod +x ~/gitea-swarm/backup-gitea.sh
( crontab -l 2>/dev/null; echo "35 2 * * * ~/gitea-swarm/backup-gitea.sh" ) | crontab -

If the copy button does not work in your browser/editor, manually select the command block and copy it.

Configuration and secret-handling best practices

Treat your Git platform as core infrastructure, not a sidecar service. That means controlling change velocity and protecting secrets as first-class operational concerns.

  • Store high-value credentials only in Swarm secrets, not in repository files.
  • Rotate DB and internal tokens after personnel changes and on a fixed cadence.
  • Keep TLS and domain configuration explicit; avoid wildcard routing unless intentional.
  • Enable email notifications for account verification and incident communication.
  • Document emergency admin access and break-glass procedure.

For teams running CI runners, create scoped credentials per runner group. Avoid global tokens that allow wide lateral movement if a single machine is compromised. Production hardening is often about reducing blast radius before an incident, not after one.

As usage increases, add worker nodes and monitor capacity around CPU, IO latency, and repository size growth. Scaling from one manager to a small cluster is straightforward when the original deployment is already service-oriented and declarative.

Verification checklist

  • HTTPS certificate is valid for git.yourdomain.com.
  • Admin login works and you can create, clone, and push to a test repository.
  • Service logs show no persistent restart loop.
  • Backup script produces expected files and can be restored in staging.
  • Firewall exposes only required ports.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

If the copy button does not work in your browser/editor, manually select the command block and copy it.

After every update (OS patches, image upgrades, DNS changes), run a short smoke test: UI login, Git clone/push, webhook delivery, and backup script execution. This catches regressions early and keeps maintenance low-risk.

Common issues and fixes

TLS certificate does not issue

Usually DNS or inbound firewall is wrong. Confirm hostname resolution and reachability on ports 80/443 before changing Traefik settings.

Gitea starts but cannot connect to DB

Validate secret names and DB host. In this stack, database host should be postgres on the shared overlay network.

Service tasks keep restarting

Use docker stack ps scm --no-trunc and service logs to identify failed config, missing secrets, or permissions issues on mounted volumes.

Git over SSH fails

Check firewall/security-group rules and verify SSH-related settings in Gitea configuration. Make sure client clone URLs match your final domain/port plan.

Backups run but recovery fails

Schedule regular restore drills in staging. Backups without tested restoration are an operational blind spot.

FAQ

Is Docker Swarm still a valid choice for production Git hosting?

Yes. For many teams, Swarm offers enough orchestration and operational simplicity while avoiding Kubernetes complexity.

Can I run this on one node first?

Yes. Start single-node, then expand with workers as load increases. Keep backups off-node from day one.

Why PostgreSQL instead of SQLite?

PostgreSQL handles concurrent production workloads more reliably and integrates better with backup/restore processes.

How frequently should I back up?

At least daily for DB and repository data, with off-site copy and monthly restore testing.

How do I reduce security risk for CI runners?

Use scoped credentials per runner and avoid broad shared tokens that can expose every repository.

What should be in a post-upgrade smoke test?

Login, repo clone, push, webhook trigger, and backup script execution. Keep this checklist documented and repeatable.

Can I migrate from another Git platform with low downtime?

Yes, if you rehearse imports, user mapping, webhook cutover, and DNS timing in staging before production switch.

Internal links

Talk to us

If you want support designing or hardening your NetBox platform, we can help with architecture, migration planning, and production readiness.

Contact Us

Deploy NetBox on Kubernetes with Helm, External PostgreSQL, and Production Guardrails
A production-first guide to running NetBox with durable storage, secret hygiene, upgrades, and operational checks.