- Remove upload-artifact step (unsupported on Gitea GHES) - Replace architecture+audit jobs with unified validate job running validate_all.py - Update docs: DEPLOY_HOST must be 172.17.0.1 (Docker bridge), not 127.0.0.1 - Add ufw rule for Docker bridge network SSH access Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.1 KiB
Gitea CI/CD Deployment Guide
This document describes how to self-host Gitea on an external server and migrate CI/CD from GitLab to Gitea Actions (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 GitLab
- Low resource usage compared to GitLab
1. Server Prerequisites
# 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:
mkdir -p ~/gitea && cd ~/gitea
Create docker-compose.yml:
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=<CHANGE_ME>
- 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: <CHANGE_ME>
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: <RUNNER_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 <CHANGE_ME> with a strong database password and <RUNNER_TOKEN> with the token from step 4.
Start Gitea and the database first:
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)
sudo apt install -y nginx certbot python3-certbot-nginx
Create /etc/nginx/sites-available/gitea:
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:
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
- Go to Site Administration > Runners in the Gitea web UI.
- Click Create new Runner and copy the registration token.
- Paste the token into
docker-compose.ymlasGITEA_RUNNER_REGISTRATION_TOKEN. - Start the runner:
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)
# 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)
- In Gitea, click + > New Migration.
- Select GitLab as the source.
- Enter your GitLab URL and a Personal Access Token.
- Gitea will import the repository, issues, labels, milestones, and merge requests.
6. CI/CD — GitLab vs Gitea Actions
The workflow file lives in .gitea/workflows/ci.yml (already created in this repository).
GitLab CI (.gitlab-ci.yml) |
Gitea Actions (.gitea/workflows/ci.yml) |
|---|---|
stages: + stage: per job |
Jobs run in parallel; use needs: for ordering |
services: (top-level on job) |
services: nested under each job with options: |
allow_failure: true |
continue-on-error: true |
rules: - if: |
on: triggers + if: conditionals per job |
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:
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:
- Stash local changes (preserves
.envand other server-side edits) - Pull latest code (
--ff-only) - Pop stash to restore local changes
- Rebuild and restart Docker containers (
docker compose --profile full up -d --build) - Run database migrations (
alembic upgrade heads) - Health check
http://localhost:8001/healthwith retries
See Hetzner Server Setup — Step 16 for the full setup guide including SSH key generation and Gitea secrets configuration.
10. Firewall Configuration
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
# 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
12. Removing GitLab (After Migration)
Once you have verified everything works on Gitea:
- Update your local git remote:
git remote set-url origin ssh://git@git.yourdomain.com:2222/your-username/letzshop-product-import.git - The
.gitlab-ci.ymlfile can be removed from the repository. - Archive or delete the GitLab project.