Some checks failed
CI / ruff (push) Successful in 8s
CI / pytest (push) Successful in 36m19s
CI / architecture (push) Successful in 11s
CI / dependency-scanning (push) Successful in 27s
CI / audit (push) Successful in 9s
CI / docs (push) Failing after 59s
CI / deploy (push) Failing after 3s
Deploy job SSHes to production after ruff/pytest/architecture pass, running scripts/deploy.sh (stash, pull, docker rebuild, migrate, health check). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
309 lines
8.5 KiB
Markdown
309 lines
8.5 KiB
Markdown
# 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
|
|
|
|
```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=<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:
|
|
|
|
```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 **GitLab** as the source.
|
|
3. Enter your GitLab URL and a Personal Access Token.
|
|
4. 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` |
|
|
| `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` | Production server IP (e.g. `127.0.0.1`) | 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 |
|
|
|
|
---
|
|
|
|
## 8. Pipeline Overview
|
|
|
|
The CI pipeline (`.gitea/workflows/ci.yml`) runs:
|
|
|
|
```
|
|
push/PR to master
|
|
├── ruff (lint)
|
|
├── pytest (tests + PostgreSQL service)
|
|
├── architecture (architecture validation)
|
|
├── dependency-scanning (pip-audit, non-blocking)
|
|
├── audit (custom audit, non-blocking)
|
|
├── docs (mkdocs build, master-only, after lint+test pass)
|
|
└── deploy (SSH deploy, master-only, after lint+test+arch pass)
|
|
```
|
|
|
|
All jobs run in parallel except `docs` and `deploy`, which wait for `ruff`, `pytest`, and `architecture` 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, architecture]
|
|
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'
|
|
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
|
|
```
|
|
|
|
---
|
|
|
|
## 12. Removing GitLab (After Migration)
|
|
|
|
Once you have verified everything works on Gitea:
|
|
|
|
1. Update your local git remote:
|
|
```bash
|
|
git remote set-url origin ssh://git@git.yourdomain.com:2222/your-username/letzshop-product-import.git
|
|
```
|
|
2. The `.gitlab-ci.yml` file can be removed from the repository.
|
|
3. Archive or delete the GitLab project.
|