Update infra docs with complete working setup
All checks were successful
Deploy to NAS / deploy (push) Successful in 44s

- README.md: rewrite with accurate setup steps including all lessons learned
- CLAUDE.md: update with working CI/CD patterns and known pitfalls
- gitea-actions.md: new file documenting Gitea Actions usage, expressions,
  act_runner config, and troubleshooting for future projects

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christoph K.
2026-04-07 18:46:55 +02:00
parent e2419411fa
commit ad65102fdc
3 changed files with 382 additions and 226 deletions

View File

@@ -1,155 +1,124 @@
# CLAUDE.md — Pamietnik Infrastruktur
# CLAUDE.md — NAS Infrastruktur
Diese Datei ist Kontext für eine dedizierte Infra-Session. Hier ist alles beschrieben, was zum Aufbau und Betrieb der Infrastruktur auf der Synology NAS notwendig ist.
Kontext für Claude Code Sessions zum Thema Deployment und Infrastruktur auf der Synology NAS.
---
## Umgebung
- **NAS:** Synology DiskStation, DSM 7.x
- **Docker-Datenpfad:** `/volume2/docker/` (alle Container-Daten hier ablegen)
- **Docker-Socket:** `/var/run/docker.sock`
- **Gitea:** läuft als Docker-Container auf der NAS, erreichbar unter `http://localhost:3000`
- **SSH-Zugriff:** `ssh jacek@<NAS-IP>`, sudo-Befehle erforderlich für Docker
- **NAS:** Synology DiskStation, DSM 7.x, IP: `192.168.1.4`
- **SSH:** `ssh jacek@192.168.1.4` — Docker-Befehle erfordern `sudo`
- **Docker-Datenpfad:** `/volume2/docker/` — alle persistenten Daten hier
- **Docker-Binary auf NAS:** `/usr/local/bin/docker`
- **Gitea:** `http://192.168.1.4:3000` (Docker-Container auf NAS)
---
## Architektur
## Laufende Dienste
```
Synology NAS
├── Gitea (Docker) :3000 — Git-Server + CI/CD
├── act_runner (Docker) — Gitea Actions Runner
├── /volume2/docker/shared/
│ └── postgres:16-alpine :5433 — Geteilte DB für alle Projekte
└── /volume2/docker/pamietnik/
└── api (Go + SPA) :9050 — Pamietnik App
| Dienst | Container | Port | Pfad |
|--------|-----------|------|------|
| PostgreSQL | `shared-postgres-1` | 5433 | `/volume2/docker/shared/` |
| act_runner | `gitea-runner` | — | `/volume2/docker/gitea-runner/` |
| Pamietnik | `pamietnik-api-1` | 9050 | `/volume2/docker/pamietnik/` |
---
## Datenbankzugriff
**Aus Containern:** `host-gateway:5433`
(`host-gateway` ist Docker's eingebauter Alias für den Host — in `extra_hosts` und `docker-compose.yml` deklarieren)
**Remote (Heimnetz):** `psql -h 192.168.1.4 -p 5433 -U <user> -d <db>`
**Direkt auf NAS:**
```bash
sudo docker exec -it shared-postgres-1 psql -U postgres
```
**Verbindung App → Datenbank:** über `host-gateway:5433` (Docker-interner Alias für den NAS-Host)
---
## Verzeichnisse auf der NAS
| Pfad | Inhalt |
|------|--------|
| `/volume2/docker/shared/pgdata` | PostgreSQL-Daten (persistent) |
| `/volume2/docker/shared/.env` | Secrets: `POSTGRES_PASSWORD` |
| `/volume2/docker/shared/docker-compose.yml` | Shared Stack (Postgres + pgAdmin) |
| `/volume2/docker/pamietnik/uploads` | Hochgeladene Bilder (persistent) |
| `/volume2/docker/pamietnik/.env` | `DB_PASSWORD`, `APP_PORT` |
| `/volume2/docker/pamietnik/docker-compose.yml` | Wird via CI/CD aus dem Repo kopiert |
| `/volume2/docker/gitea-runner/` | act_runner Konfiguration & Daten |
---
## Shared Stack (`/volume2/docker/shared/docker-compose.yml`)
```yaml
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
ports:
- "5433:5432"
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
volumes:
- /volume2/docker/shared/pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
interval: 5s
timeout: 5s
retries: 5
```
---
## Pamietnik Stack (`docker-compose.yml` im Repo-Root)
```yaml
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "${APP_PORT:-9050}:8080"
extra_hosts:
- "host-gateway:host-gateway"
environment:
DATABASE_URL: postgres://${DB_USER:-pamietnik}:${DB_PASSWORD:?DB_PASSWORD is required}@host-gateway:5433/${DB_NAME:-pamietnik}?sslmode=disable
LISTEN_ADDR: :8080
UPLOAD_DIR: /uploads
volumes:
- /volume2/docker/pamietnik/uploads:/uploads
restart: unless-stopped
**Wichtig PostgreSQL 15+:** Nach `GRANT ALL PRIVILEGES ON DATABASE` zusätzlich:
```sql
GRANT ALL ON SCHEMA public TO <user>;
```
---
## CI/CD: Gitea Actions
**Workflow:** `.gitea/workflows/deploy.yml` — wird bei Push auf `main` ausgeführt.
**Workflow-Datei:** `.gitea/workflows/deploy.yml`
**Trigger:** Push auf `main`
**Runner:** `gitea/act_runner` Container auf der NAS mit `--network host` und Docker-Socket-Mount.
**Runner-Setup:**
- Container: `gitea/act_runner:latest` mit `--network host`
- `GITEA_INSTANCE_URL` muss NAS-IP sein (`192.168.1.4:3000`), **nicht** `localhost`
- `config.yaml` braucht `valid_volumes: [/volume2/docker]` sonst werden Mounts ignoriert
**Gitea Secrets** (Repository → Einstellungen → Actions → Secrets):
- `DB_PASSWORD` — Passwort des `pamietnik` DB-Users
- `DEPLOY_DIR``/volume2/docker/pamietnik`
**Job-Container:** `docker:latest` mit `-v /volume2/docker:/volume2/docker`
- Docker CLI ist im Image enthalten
- Socket wird automatisch vom Runner propagiert (nicht nochmal in `options` mounten → Duplicate-Fehler)
- `wget` statt `curl` verwenden (`curl` nicht im Image)
**Gitea Variables** (Repository → Einstellungen → Actions → Variables):
- `DB_USER``pamietnik`
- `DB_NAME``pamietnik`
- `APP_PORT``9050`
**Gitea Konfiguration:**
- Nicht-sensitive Werte (Pfade, Ports, DB-Namen) → **Variables** (`vars.NAME`)
- Passwörter, Tokens → **Secrets** (`secrets.NAME`)
- Variables mit Leerzeichen am Ende → Parsing-Fehler in Expressions
---
## Dienste & URLs
## Deployment-Muster für neue Projekte
| Dienst | URL |
|--------|-----|
| Pamietnik | `http://<NAS-IP>:9050` |
| Gitea | `http://<NAS-IP>:3000` |
| PostgreSQL | `<NAS-IP>:5433` |
```yaml
# docker-compose.yml im Repo
services:
api:
build: .
ports:
- "${APP_PORT:-8080}:8080"
extra_hosts:
- "host-gateway:host-gateway"
environment:
DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@host-gateway:5433/${DB_NAME}
volumes:
- /volume2/docker/<projekt>/uploads:/uploads
restart: unless-stopped
```
---
## Wichtige Befehle
```bash
# Shared Stack starten
cd /volume2/docker/shared && sudo docker compose up -d
# Logs
sudo docker compose -f /volume2/docker/shared/docker-compose.yml logs -f
sudo docker compose -f /volume2/docker/pamietnik/docker-compose.yml logs -f api
# Datenbank-User anlegen
sudo docker exec -it shared-postgres-1 psql -U postgres
# Backup
sudo docker exec shared-postgres-1 pg_dump -U postgres pamietnik \
> /volume2/docker/shared/backup_$(date +%Y%m%d).sql
```yaml
# .gitea/workflows/deploy.yml
jobs:
deploy:
runs-on: self-hosted
container:
image: docker:latest
options: -v /volume2/docker:/volume2/docker
steps:
- name: Pull code
run: |
if [ -d "${{ vars.DEPLOY_DIR }}/.git" ]; then
git -C ${{ vars.DEPLOY_DIR }} pull
else
git clone http://192.168.1.4:3000/<org>/<repo>.git ${{ vars.DEPLOY_DIR }}
fi
- name: Write .env
run: printf 'DB_PASSWORD=%s\n' '${{ secrets.DB_PASSWORD }}' > ${{ vars.DEPLOY_DIR }}/.env
- name: Build & Deploy
run: docker compose -f ${{ vars.DEPLOY_DIR }}/docker-compose.yml up --build -d
- name: Health check
run: sleep 15 && wget -qO- http://192.168.1.4:${{ vars.APP_PORT }}/healthz || exit 1
```
---
## Synology-spezifische Hinweise
## Bekannte Fallstricke
- Docker-Befehle erfordern `sudo`
- Ports unter 1024 können Container nicht binden → `PGADMIN_LISTEN_PORT: 8080` nötig
- pgAdmin-Verzeichnis braucht UID 5050: `sudo chown -R 5050:5050 /volume2/docker/shared/pgadmin`
- Docker-Socket ist unter `/var/run/docker.sock` erreichbar
- Container Manager UI unterstützt keinen Datei-Mount für den Docker-Socket → `docker run` via SSH nutzen
- **`Secure: true` auf Session-Cookies** schlägt fehl bei HTTP → `Secure: false` setzen
- **Ports unter 1024** können Container auf Synology nicht binden → internen Port > 1024 wählen
- **`localhost`** im Job-Container zeigt auf den Container selbst, nicht auf die NAS
- **Volume-Mounts** in Workflow `container.options` werden doppelt gemountet wenn identisch mit Runner-Mount
- **`/volume2/docker`** muss in `valid_volumes` der `config.yaml` stehen sonst wird Mount ignoriert
---
## Vollständige Setup-Anleitung
Siehe `infra/README.md` im Repo.
Siehe `infra/README.md`