# Gitea CI/CD Deployment Guide This document describes how to **self-host Gitea** on an external server with **Gitea Actions** CI/CD (GitHub Actions-compatible). --- ## Why Gitea? - Lightweight, self-hosted Git forge (single binary or Docker image) - Built-in CI/CD via **Gitea Actions** (GitHub Actions-compatible YAML) - Built-in migration tool imports repos, issues, and PRs from other forges - Low resource usage --- ## 1. Server Prerequisites ```bash # Ubuntu/Debian sudo apt update && sudo apt install -y docker.io docker-compose-plugin sudo systemctl enable --now docker sudo usermod -aG docker $USER # log out/in after this ``` --- ## 2. Gitea Server Setup (Docker Compose) Create a directory on your server: ```bash mkdir -p ~/gitea && cd ~/gitea ``` Create `docker-compose.yml`: ```yaml services: gitea: image: gitea/gitea:latest container_name: gitea restart: always environment: - USER_UID=1000 - USER_GID=1000 - GITEA__database__DB_TYPE=postgres - GITEA__database__HOST=gitea-db:5432 - GITEA__database__NAME=gitea - GITEA__database__USER=gitea - GITEA__database__PASSWD= - GITEA__server__ROOT_URL=https://git.yourdomain.com/ - GITEA__server__SSH_DOMAIN=git.yourdomain.com - GITEA__server__DOMAIN=git.yourdomain.com - GITEA__actions__ENABLED=true volumes: - gitea-data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro ports: - "3000:3000" # Web UI - "2222:22" # SSH (Git over SSH) depends_on: gitea-db: condition: service_healthy gitea-db: image: postgres:15 container_name: gitea-db restart: always environment: POSTGRES_DB: gitea POSTGRES_USER: gitea POSTGRES_PASSWORD: volumes: - gitea-db-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U gitea"] interval: 10s timeout: 5s retries: 5 # Gitea Actions runner — executes CI workflows gitea-runner: image: gitea/act_runner:latest container_name: gitea-runner restart: always environment: GITEA_INSTANCE_URL: http://gitea:3000 GITEA_RUNNER_REGISTRATION_TOKEN: GITEA_RUNNER_NAME: default-runner volumes: - /var/run/docker.sock:/var/run/docker.sock - gitea-runner-data:/data depends_on: - gitea volumes: gitea-data: gitea-db-data: gitea-runner-data: ``` !!! warning "Replace placeholders" Replace `` with a strong database password and `` with the token from step 4. Start Gitea and the database first: ```bash docker compose up -d gitea gitea-db ``` Visit `http://your-server-ip:3000` and complete the initial setup wizard. --- ## 3. Reverse Proxy with HTTPS (Nginx + Let's Encrypt) ```bash sudo apt install -y nginx certbot python3-certbot-nginx ``` Create `/etc/nginx/sites-available/gitea`: ```nginx server { server_name git.yourdomain.com; location / { proxy_pass http://127.0.0.1:3000; 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; client_max_body_size 100M; } } ``` Enable and get a certificate: ```bash sudo ln -s /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/ sudo certbot --nginx -d git.yourdomain.com sudo systemctl reload nginx ``` --- ## 4. Enable Actions & Register the Runner 1. Go to **Site Administration > Runners** in the Gitea web UI. 2. Click **Create new Runner** and copy the registration token. 3. Paste the token into `docker-compose.yml` as `GITEA_RUNNER_REGISTRATION_TOKEN`. 4. Start the runner: ```bash docker compose up -d gitea-runner ``` Verify the runner appears as **Online** in the admin panel. --- ## 5. Migrate Your Repository ### Option A: Git push (code only) ```bash # On your local machine cd /path/to/letzshop-product-import git remote add gitea ssh://git@git.yourdomain.com:2222/your-username/letzshop-product-import.git git push gitea --all git push gitea --tags ``` ### Option B: Gitea built-in migration (code + issues + PRs) 1. In Gitea, click **+** > **New Migration**. 2. Select the source forge (GitHub, GitLab, etc.). 3. Enter the source URL and a Personal Access Token. 4. Gitea will import the repository, issues, labels, milestones, and pull/merge requests. --- ## 6. CI/CD — Gitea Actions The workflow file lives in `.gitea/workflows/ci.yml` (already created in this repository). Gitea Actions uses GitHub Actions-compatible YAML syntax. | `artifacts: paths:` | `actions/upload-artifact@v4` (not supported on Gitea GHES) | | `cache: paths:` | `actions/cache@v4` | | `coverage: '/regex/'` | Use coverage action or parse in step | | CI/CD Variables (UI) | Repository **Settings > Secrets** | --- ## 7. CI/CD Secrets Configure these in your Gitea repository under **Settings > Actions > Secrets**: | Secret | Description | Used by | |--------|-------------|---------| | `DEPLOY_SSH_KEY` | Ed25519 private key for deployment | Deploy job | | `DEPLOY_HOST` | Docker bridge gateway IP: `172.17.0.1` (see note below) | Deploy job | | `DEPLOY_USER` | SSH user on production server (e.g. `samir`) | Deploy job | | `DEPLOY_PATH` | App directory on server (e.g. `/home/samir/apps/orion`) | Deploy job | !!! important "DEPLOY_HOST must be `172.17.0.1`, not `127.0.0.1`" The Gitea Actions runner executes CI jobs inside Docker containers. From inside the container, `127.0.0.1` refers to the container itself, not the host machine. Use `172.17.0.1` (the Docker bridge gateway) so the SSH action can reach the host's SSH daemon. When Gitea and Orion are split onto separate servers, update this to the Orion server's real IP. --- ## 8. Pipeline Overview The CI pipeline (`.gitea/workflows/ci.yml`) runs: ``` push/PR to master ├── ruff (lint) ├── pytest (tests + PostgreSQL service) ├── validate (all 4 validators: architecture, security, performance, audit) ├── dependency-scanning (pip-audit, non-blocking) ├── docs (mkdocs build, master-only, after lint+test+validate pass) └── deploy (SSH deploy, master-only, after lint+test+validate pass) ``` All jobs run in parallel except `docs` and `deploy`, which wait for `ruff`, `pytest`, and `validate` to pass. The `deploy` job only runs on push (not PRs). --- ## 9. Deploy Job (Continuous Deployment) The CI pipeline includes an automated deploy job that runs on every successful push to master. It SSHes to the production server and runs a version-controlled deploy script: ```yaml deploy: runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/master' needs: [ruff, pytest, validate] steps: - name: Deploy to production uses: appleboy/ssh-action@v1 with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_SSH_KEY }} port: 22 command_timeout: 10m script: cd ${{ secrets.DEPLOY_PATH }} && bash scripts/deploy.sh ``` The `scripts/deploy.sh` script handles the full deploy lifecycle: 1. Stash local changes (preserves `.env` and other server-side edits) 2. Pull latest code (`--ff-only`) 3. Pop stash to restore local changes 4. Rebuild and restart Docker containers (`docker compose --profile full up -d --build`) 5. Run database migrations (`alembic upgrade heads`) 6. Health check `http://localhost:8001/health` with retries See [Hetzner Server Setup — Step 16](hetzner-server-setup.md#step-16-continuous-deployment) for the full setup guide including SSH key generation and Gitea secrets configuration. --- ## 10. Firewall Configuration ```bash sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' # Allow CI containers (Docker bridge) to SSH to the host for deployment sudo ufw allow from 172.17.0.0/16 to any port 22 sudo ufw enable ``` --- ## 11. Maintenance ```bash # View Gitea logs docker compose -f ~/gitea/docker-compose.yml logs -f gitea # View runner logs docker compose -f ~/gitea/docker-compose.yml logs -f gitea-runner # Update Gitea cd ~/gitea docker compose pull docker compose up -d # Backup Gitea data docker run --rm -v gitea-data:/data -v $(pwd):/backup alpine \ tar czf /backup/gitea-backup-$(date +%Y%m%d).tar.gz /data ``` ---