---
title: "Gitea Server Setup and Config"
description: "Complete setup and configuration reference for the self-hosted Gitea instance on LXC 225 (10.10.0.225). Covers service management, Gitea Actions CI/CD runner setup, shared composite actions, branch protection, backup/restore, and troubleshooting."
type: reference
domain: server-configs
tags: [gitea, git, ci-cd, gitea-actions, lxc, postgresql, docker-runner, branch-protection]
---

# Gitea - Self-Hosted Git Server

**LXC 225** | **10.10.0.225** | **git.manticorum.com**

Self-hosted Git server with web UI, Git LFS support, and Gitea Actions for CI/CD pipelines.

## Quick Info

| Property | Value |
|----------|-------|
| **Type** | LXC Container (Proxmox) |
| **OS** | Ubuntu 20.04 LTS |
| **IP** | 10.10.0.225 |
| **Public URL** | https://git.manticorum.com |
| **Gitea Version** | 1.22.6 |
| **Database** | PostgreSQL 12 |
| **Reverse Proxy** | Nginx Proxy Manager (10.10.0.16) |

## Container Specs

- **VMID**: 225
- **CPU**: 2 cores
- **RAM**: 2GB
- **Disk**: 20GB
- **Features**: Nesting enabled (for future Docker runner support)

## Services

### Gitea Web
- **Port**: 3000 (internal)
- **Service**: `gitea.service`
- **User**: `git`
- **Work Dir**: `/var/lib/gitea`
- **Config**: `/etc/gitea/app.ini`
- **Data**: `/var/lib/gitea/data`
- **Logs**: `/var/lib/gitea/log`

### PostgreSQL
- **Version**: 12
- **Port**: 5432 (localhost only)
- **Database**: `gitea`
- **User**: `gitea`
- **Service**: `postgresql`

## Management

### Access Container
```bash
ssh root@10.10.0.225
# or via Proxmox
pct enter 225
```

### Service Management
```bash
# Status
systemctl status gitea
systemctl status postgresql

# Restart
systemctl restart gitea

# Logs
journalctl -u gitea -f
```

### Database Access
```bash
# As postgres user
sudo -u postgres psql -d gitea

# As gitea user (from container)
PGPASSWORD=gitea123 psql -U gitea -d gitea -h 127.0.0.1
```

## Configuration

### Main Config File
`/etc/gitea/app.ini` contains all Gitea settings:
- Database connection
- Server domain and URLs
- SSH settings
- LFS configuration
- OAuth2/JWT secrets
- Actions enabled

**Permissions**:
- Owner: `root:git`
- Mode: `640`
- Directory: `750` on `/etc/gitea`

### Admin Account
- **Username**: `cal`
- **Password**: Set during initial setup (change immediately!)
- **Email**: `cal@manticorum.com`

### Features Enabled
- ✅ **Gitea Actions** - Built-in CI/CD (GitHub Actions compatible)
- ✅ **Git LFS** - Large file storage support
- ✅ **SSH Access** - Git over SSH on port 22
- ✅ **Web UI** - Repository browser and management
- ✅ **Organizations** - Multi-user repository groups
- ✅ **Webhooks** - Integration with external services

### Key app.ini Settings (Actions)
```ini
[actions]
DEFAULT_ACTIONS_URL = self   # Short-form actions resolve against local Gitea instance

[service]
REQUIRE_SIGNIN_VIEW = false  # Public repos (like gitea-actions) cloneable without auth
```

- `DEFAULT_ACTIONS_URL=self` means `uses: cal/gitea-actions/calver@main` resolves locally
- `REQUIRE_SIGNIN_VIEW=false` only exposes PUBLIC repos — all private repos remain locked down
- The only public repo is `cal/gitea-actions` (reusable CI composite actions, no sensitive code)

## Backup

### What to Backup
1. **PostgreSQL database**: `gitea` database
2. **Repository data**: `/var/lib/gitea/data/gitea-repositories`
3. **Configuration**: `/etc/gitea/app.ini`
4. **Custom files**: `/var/lib/gitea/custom` (if any)

### Backup Commands
```bash
# Database dump
sudo -u postgres pg_dump gitea > gitea-backup-$(date +%Y%m%d).sql

# Full data directory
tar -czf gitea-data-$(date +%Y%m%d).tar.gz /var/lib/gitea

# Config only
cp /etc/gitea/app.ini gitea-app-$(date +%Y%m%d).ini
```

### Restore
```bash
# Restore database
sudo -u postgres psql -d gitea < gitea-backup.sql

# Restore data
tar -xzf gitea-data.tar.gz -C /
chown -R git:git /var/lib/gitea
```

## Upgrades

### Upgrade Gitea
```bash
# Stop service
systemctl stop gitea

# Backup current binary
cp /usr/local/bin/gitea /usr/local/bin/gitea.backup

# Download new version
wget -O /usr/local/bin/gitea https://dl.gitea.com/gitea/VERSION/gitea-VERSION-linux-amd64

# Set permissions
chmod +x /usr/local/bin/gitea

# Start service (will auto-migrate database)
systemctl start gitea

# Check logs
journalctl -u gitea -f
```

### Check Version
```bash
/usr/local/bin/gitea --version
```

## Setting Up CI/CD with Gitea Actions

Gitea Actions are enabled and the runner is deployed on the same LXC.

### Runner Container

```bash
# Current production runner setup
docker run -d \
  --name gitea-runner \
  --restart unless-stopped \
  -e GITEA_RUNNER_REGISTRATION_TOKEN=<token-from-gitea-admin> \
  -e GITEA_INSTANCE_URL=http://10.10.0.225:3000 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v gitea-runner-data:/data \
  -v /etc/gitea/runner-config.yaml:/config.yaml:ro \
  gitea/act_runner:latest
```

**CRITICAL**: `GITEA_INSTANCE_URL` must be the **internal** URL (`http://10.10.0.225:3000`), not the public domain. The runner uses this for API communication and auth token matching.

### Runner Config (`/etc/gitea/runner-config.yaml`)

Key settings:
- `container.options: --add-host=git.manticorum.com:host-gateway` — lets job containers resolve the public domain
- `container.force_pull: true` — always pulls latest runner images
- Labels: `ubuntu-latest`, `ubuntu-22.04`, `ubuntu-20.04`

### Shared Composite Actions (`cal/gitea-actions`)

Reusable CI/CD actions shared across all projects. This repo is **public** (required for runner auth).

| Action | Purpose |
|--------|---------|
| `cal/gitea-actions/calver@main` | Generate CalVer version (YYYY.MM.BUILD) from git tags |
| `cal/gitea-actions/gitea-tag@main` | Create git tag via Gitea API |
| `cal/gitea-actions/discord-notify@main` | Send Discord embed notification via webhook |

### Action Reference Rules

Because `DEFAULT_ACTIONS_URL=self` is set:

```yaml
# Local actions (cal/gitea-actions) — use SHORT FORM
uses: cal/gitea-actions/calver@main

# GitHub actions — use FULL URL (otherwise they'd resolve against local Gitea)
uses: https://github.com/actions/checkout@v4
uses: https://github.com/docker/setup-buildx-action@v3
uses: https://github.com/docker/login-action@v3
uses: https://github.com/docker/build-push-action@v5
```

### Creating a New Workflow

1. Copy an existing workflow as a template (e.g., from major-domo-database)
2. Use short form for `cal/gitea-actions/*` references
3. Use `https://github.com/` prefix for all GitHub action references
4. Required secrets: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN`, `DISCORD_WEBHOOK`

## Adding Repositories

### Via Web UI
1. Go to https://git.manticorum.com
2. Click "+" → "New Repository"
3. Fill in details and create

### Via Command Line
```bash
# Add remote
git remote add homelab git@git.manticorum.com:cal/repo-name.git

# Or HTTPS
git remote add homelab https://git.manticorum.com/cal/repo-name.git

# Push
git push homelab main
```

### Migrate from GitHub
Gitea has built-in migration:
1. New Repository → "Migrate from GitHub"
2. Enter GitHub URL and token
3. Gitea will clone all commits, branches, tags

## Integration with NPM

Reverse proxy is configured on NPM (10.10.0.16):
- **Domain**: git.manticorum.com
- **Forward to**: 10.10.0.225:3000
- **SSL**: Let's Encrypt
- **Websockets**: Enabled

## Troubleshooting

### Gitea won't start
```bash
# Check logs
journalctl -u gitea -n 50

# Common issues:
# - Permission on /etc/gitea/app.ini (should be 640, root:git)
# - PostgreSQL not running
# - Port 3000 already in use
```

### Can't connect to database
```bash
# Check PostgreSQL is running
systemctl status postgresql

# Test connection
PGPASSWORD=gitea123 psql -U gitea -d gitea -h 127.0.0.1 -c "SELECT 1;"

# Check pg_hba.conf allows md5 auth
cat /etc/postgresql/12/main/pg_hba.conf | grep md5
```

### 502 Bad Gateway on web
```bash
# Check Gitea is listening
ss -tlnp | grep 3000

# Check NPM can reach container
curl http://10.10.0.225:3000

# Verify firewall rules (should allow from 10.10.0.0/24)
```

### Actions runner not working
- Ensure runner is registered in Gitea Admin → Actions → Runners
- Check runner logs: `docker logs gitea-runner`
- Verify `GITEA_INSTANCE_URL` uses internal URL (`http://10.10.0.225:3000`), NOT the public domain
- Ensure runner has network access to Gitea

### Actions can't clone composite actions ("authentication required")
- Verify `REQUIRE_SIGNIN_VIEW = false` in app.ini (allows public repo clone without auth)
- Verify `DEFAULT_ACTIONS_URL = self` in app.ini
- Use short-form references for local actions (`cal/gitea-actions/calver@main`)
- Use full GitHub URLs for GitHub actions (`https://github.com/actions/checkout@v4`)
- The `cal/gitea-actions` repo must be set to **public** visibility

## Security Notes

- Database password is stored in `/etc/gitea/app.ini` (secured with 640 permissions)
- SSH keys for Git access are stored per-user in Gitea database
- JWT secrets are auto-generated and stored in config
- LXC is unprivileged for better isolation
- PostgreSQL only listens on localhost
- `REQUIRE_SIGNIN_VIEW=false` — only public repos are accessible without login; all private repos remain fully protected
- The only public repo is `cal/gitea-actions` (audited — contains no secrets or sensitive code)

## Related Documentation

- [Official Gitea Docs](https://docs.gitea.io/)
- [Gitea Actions](https://docs.gitea.io/en-us/usage/actions/overview/)
- [Proxmox LXC Config](../proxmox/lxc/225.conf)
- [Networking Setup](../../networking/CONTEXT.md)

## Deployment Date

**Created**: 2026-02-03
**By**: Claude Code (Proxmox Skill)
**Initial Version**: Gitea 1.22.6 on Ubuntu 20.04


## Git Remotes

This repository is mirrored on both GitHub and Gitea for redundancy:
- **GitHub**: https://github.com/calcorum/claude-home
- **Gitea**: https://git.manticorum.com/cal/claude-home

---

## Branch Protection Manager

Automated script to apply consistent branch protection rules across all your Gitea repositories.

### Features

- Applies branch protection rules to all repositories for a user/organization
- Supports dry-run mode to preview changes
- Configurable protection rules via environment variables
- Handles updating existing protection rules
- Clear success/error reporting

### Requirements

```bash
pip install requests
```

### Configuration

The script (`apply_branch_protection.py`) uses these settings:
- ✅ Disable direct pushes (force PR workflow)
- ✅ Require 1 approval before merge
- ✅ Restrict approvals to whitelisted users
- ✅ Dismiss stale approvals when new commits are pushed
- ✅ Enable status checks (for CI/CD)
- ✅ Restrict merging to whitelisted users
- ✅ Block merge on rejected reviews
- ✅ Block merge if pull request is outdated (critical for DB migrations)

### Usage

#### 1. Create a Gitea API Token

1. Go to https://git.manticorum.com/user/settings/applications
2. Click "Generate New Token"
3. Give it a name (e.g., "Branch Protection Script")
4. Select permissions: `repo` (full control)
5. Click "Generate Token"
6. Copy the token (you won't see it again!)

#### 2. Set Environment Variables

```bash
export GITEA_TOKEN='your-api-token-here'
export GITEA_URL='https://git.manticorum.com'  # Optional, defaults to this
export GITEA_OWNER='cal'  # Optional, defaults to cal
```

#### 3. Run the Script

**Dry run (preview changes without applying):**
```bash
cd /mnt/NV2/Development/claude-home/server-configs/gitea
python apply_branch_protection.py --dry-run
```

**Apply to all repositories:**
```bash
python apply_branch_protection.py
```

### Customizing Protection Rules

Edit the `BranchProtectionConfig` section in `main()` to customize the rules:

```python
config = BranchProtectionConfig(
    branch_name="main",              # Branch to protect
    enable_push=False,               # Disable direct pushes
    required_approvals=1,            # Number of required approvals
    enable_approvals_whitelist=True, # Restrict who can approve
    approvals_whitelist_usernames=[GITEA_OWNER],
    dismiss_stale_approvals=True,    # Dismiss approvals on new commits
    enable_status_check=True,        # Require status checks to pass
    enable_merge_whitelist=True,     # Restrict who can merge
    merge_whitelist_usernames=[GITEA_OWNER],
    block_on_rejected_reviews=True,  # Block merge if reviews are rejected
    block_on_outdated_branch=True,   # Block merge if branch is outdated
    require_signed_commits=False,    # Require GPG signatures
)
```

### Example Output

```
============================================================
Gitea Branch Protection Configuration
============================================================
Gitea URL: https://git.manticorum.com
Owner: cal
Branch: main
============================================================
Protection Rules:
  • Direct pushes: Disabled
  • Required approvals: 1
  • Approvals whitelist: cal
  • Dismiss stale approvals: True
  • Status checks enabled: True
  • Merge whitelist: cal
  • Block on rejected reviews: True
  • Block on outdated branch: True
  • Require signed commits: False
============================================================

Applying branch protection to 5 repositories...

📦 major-domo-database
   🔄 Updating existing protection...
   ✅ Successfully applied branch protection
📦 major-domo-bot
   ✅ Successfully applied branch protection
...
```

### Troubleshooting Branch Protection Script

**"GITEA_TOKEN environment variable is required"**
- You need to set your API token. See usage step 2 above.

**"Error fetching repositories"**
- Check that your `GITEA_URL` is correct
- Verify your API token has `repo` permissions
- Ensure the `GITEA_OWNER` username is correct

**"Could not delete existing protection"**
- The script will still try to create the new protection
- If this fails, manually delete the old protection rule from Gitea web UI

**"Error: 422 Unprocessable Entity"**
This usually means:
- The branch doesn't exist in the repository
- Invalid username in whitelist
- Conflicting protection rule settings

### API References

This script uses the Gitea API branch protection endpoints:
- [Gitea API Documentation](https://docs.gitea.com/api/)
- [Protected Branches](https://docs.gitea.com/usage/access-control/protected-branches)
- [Create Branch Protection Endpoint](https://share.apidog.com/apidoc/docs-site/346218/api-3521477)

