Files
hermes-config/skills/self-hosting/docker-service-deployment/references/gitea.md
T
2026-07-12 10:17:17 -04:00

276 lines
8.4 KiB
Markdown

# Gitea Self-Hosted Git Service
## Deployment
| Detail | Value |
|--------|-------|
| Container | `gitea/gitea:latest` |
| Data path | `/mnt/seagate8tb/docker/gitea/` |
| Internal port | 3000 (HTTP) |
| SSL port | 443 |
| Auth | Local (ray / password) |
| URL | `https://gitea.graj-media.com` |
## Docker Compose
```yaml
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: unless-stopped
volumes:
- ./data:/data
- ./config:/etc/gitea
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:3000:3000"
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__DOMAIN=gitea.graj-media.com
- GITEA__server__SSH_DOMAIN=gitea.graj-media.com
- GITEA__server__HTTP_PORT=3000
- GITEA__server__ROOT_URL=https://gitea.graj-media.com
- GITEA__server__DISABLE_SSH=true
- GITEA__server__LFS_START_SERVER=true
networks:
- gitea-net
networks:
gitea-net:
driver: bridge
```
## Nginx Config
```nginx
server {
listen 443 ssl;
server_name gitea.graj-media.com;
ssl_certificate /etc/letsencrypt/live/grajmedia.duckdns.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/grajmedia.duckdns.org/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 512M;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
}
}
```
## Critical Pitfalls
### 1. Docker Entrypoint Overrides app.ini
The Gitea Docker entrypoint rewrites `/data/gitea/conf/app.ini` from environment variables **every time the container starts**. Manual edits to app.ini are silently overwritten.
**Fix:** Use `GITEA__section__key=value` environment variables in docker-compose.yml, not direct file edits. The double-underscore delimiter maps to INI sections: `GITEA__server__DISABLE_SSH=true` sets `[server] DISABLE_SSH = true`.
After the container starts, the entrypoint generates the config from env vars first, then Gitea's web runner applies `INSTALL_LOCK = true` from the env if set.
### 2. SSH Port Conflict
The Gitea Docker image includes an OpenSSH daemon that automatically binds to port 22 inside the container. When `START_SSH_SERVER=true` (default), Gitea also tries to bind its own SSH server on port 22 → fatal error → container restarts in a crash loop.
**Fix:** Set `GITEA__server__DISABLE_SSH=true` in environment. This prevents Gitea from starting its SSH server. HTTPS cloning still works through the web interface. If SSH-based git access is needed later, configure a non-conflicting internal port with `SSH_LISTEN_PORT`.
**Detection in logs:**
```
[F] Failed to start SSH server: listen tcp :22: bind: address already in use
Received signal 15; terminating.
```
### 3. Admin Account Setup (Headless / API-First)
Gitea's install page requires browser interaction. To set up without the web UI:
1. **Stop the container** (so web server isn't running)
2. **Set `INSTALL_LOCK = true`** in app.ini (or the env will be used on next start)
3. **Run migration** to create the database schema:
```bash
docker run --rm \
-v /mnt/seagate8tb/docker/gitea/data:/data \
-v /mnt/seagate8tb/docker/gitea/config:/etc/gitea \
--user 1000:1000 \
gitea/gitea:latest \
gitea migrate
```
4. **Create admin user:**
```bash
docker run --rm \
-v /mnt/seagate8tb/docker/gitea/data:/data \
-v /mnt/seagate8tb/docker/gitea/config:/etc/gitea \
--user 1000:1000 \
gitea/gitea:latest \
gitea admin user create --username ray --password "<password>" --email ray@grajmedia.duckdns.org --admin
```
5. **Start container** — it now runs as an installed instance with the admin user ready
**Alternative** (if container must stay running): Navigate to the install page in a browser, fill admin fields, and submit. The browser form works when the install page is served.
### 4. Secret Key Generation
Generate all secrets outside the container using the Gitea binary:
```bash
gitea generate secret SECRET_KEY
gitea generate secret JWT_SECRET
gitea generate secret LFS_JWT_SECRET
gitea generate secret INTERNAL_TOKEN
```
These can be passed as env vars or written to app.ini. Write them to the config file **before** the first migration to avoid key rotation issues.
### 5. must_change_password Blocks API
If API calls return `"You must change your password"`, clear the flag:
```bash
docker exec gitea sqlite3 /data/gitea/gitea.db \
"UPDATE user SET must_change_password=0 WHERE name='ray';"
```
This happens when the password was changed via the admin CLI (`gitea admin user change-password`) but the `must_change_password` flag wasn't cleared.
## Verification
```bash
# Container health
docker ps --filter name=gitea --format '{{.Status}}'
# HTTP response
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/
# Should return 200
# SSL via nginx
curl -sk https://gitea.graj-media.com/ | grep -o '<title>[^<]*</title>'
# Login page renders
curl -sk https://gitea.graj-media.com/user/login | grep -c 'Sign In'
```
## API Token Workflow
Generate a token for automation:
```bash
docker exec -u git gitea gitea admin user generate-access-token \
--username ray --token-name "automation" --scopes "all" 2>&1 | grep -v "^$"
```
Create repos via API:
```bash
curl -s -X POST http://127.0.0.1:3000/api/v1/user/repos \
-H "Authorization: token <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"name":"my-repo","private":false}'
```
### Push code with URL-encoded password
Encode `#` as `%23`, `^` as `%5E`, `$` as `%24`:
```bash
git remote add origin http://ray:4W%23UxJ%5EacTrdPT@127.0.0.1:3000/ray/repo.git
git branch -m master main
git push -u origin main
```
## Workflow Automation
### Git Save Alias
One-command add+commit+push. Works in any repo:
```bash
git config --global alias.save '!f() { git add -A && git commit -m "$*" && git push; }; f'
```
Usage: `git save "message describing changes"`
### Daily Autosave Cron (no_agent)
Script that auto-commits any repo with pending changes at 2 AM daily. Uses Hermes `no_agent=true` cron — only produces output when changes were actually saved (silent when clean).
**Script** (`~/.hermes/scripts/git-autosave.sh`):
```bash
#!/bin/bash
set -euo pipefail
DATE=$(date '+%Y-%m-%d')
LOGFILE="$HOME/.hermes/logs/git-autosave.log"
mkdir -p "$HOME/.hermes/logs"
REPOS=(
"/mnt/seagate8tb/Websites/ShopProQuote.backup-20260626-2058/spq-v2:main"
"/mnt/seagate8tb/docker:main"
)
for entry in "${REPOS[@]}"; do
DIR="${entry%%:*}"
BRANCH="${entry##*:}"
if [ ! -d "$DIR/.git" ]; then
echo "[$DATE] SKIP $DIR — no .git" >> "$LOGFILE"
continue
fi
cd "$DIR"
if [ -z "$(git status --porcelain 2>/dev/null)" ]; then
echo "[$DATE] CLEAN $DIR" >> "$LOGFILE"
continue
fi
git pull --rebase origin "$BRANCH" 2>/dev/null || true
git add -A
git commit -m "auto-save $DATE"
git push origin "$BRANCH"
echo "[$DATE] SAVED $DIR — auto-save $DATE" >> "$LOGFILE"
done
```
**Cron job setup** (via Hermes cronjob tool):
- `no_agent=true` — the script IS the job, its stdout is delivered verbatim (or silently when empty)
- `deliver=local` — logs saved, no notification on clean runs
- `schedule=0 2 * * *` — daily at 2 AM
**Pitfall:** Remotes must use token-based auth (not password) for unattended pushes. Set up with `git remote set-url origin http://ray:<TOKEN>@127.0.0.1:3000/ray/repo.git` to avoid credential prompts.
## First Use
1. Login as `ray` with the configured password
2. Click "New Repository" from the dashboard
3. Create your first repo and push:
```bash
git init
git add .
git commit -m "initial commit"
git remote add origin https://gitea.graj-media.com/ray/<repo>.git
git push -u origin main
```
## Data Layout
```
/mnt/seagate8tb/docker/gitea/
├── docker-compose.yml
├── data/
│ ├── gitea/
│ │ ├── conf/app.ini # Config (overwritten by entrypoint)
│ │ ├── gitea.db # SQLite database
│ │ └── log/
│ └── git/
│ └── repositories/ # Actual git repos
└── config/
```