# 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/
```
