All checks were successful
Build & Deploy / deploy (push) Successful in 1m14s
Fehler: claude-haiku-4-5 lehnt output_config.effort mit einem 400 ab, jede Karte lief in einen Lesefehler. effort und der server-seitige Fallback werden jetzt nur an Modelle geschickt, die sie annehmen; ein unbekanntes Modell mit engerem Parametersatz wird einmal ohne Zusatzparameter wiederholt, statt die Karte zu verlieren. Tests decken die Zuordnung ab. Design: Grau kommt als Hierarchiestufe dazu. Drei Ebenen für Fläche (weiß / grau gefüllt / schwarz), Text (schwarz / --ink-2 / --ink-3) und Linie (3px / 1px schwarz / graue Haarlinie). Vorher trug alles dieselbe 3px-Kante und dasselbe Schwarz, dadurch war keine Ordnung erkennbar. Konkret: nicht gewählte Filter treten zurück, Listenzeilen bekommen drei Textstufen und Statuspillen statt einer gleichförmigen Zeile, das Kopfband über Listen ist grau hinterlegt, zweitrangige und zerstörende Aktionen sind leiser als die primäre. iOS: Datumsfelder zentrieren ihren Wert und sacken in der Höhe ein. Die WebKit-Pseudoelemente ziehen sie auf die Form der übrigen Felder. Dazu die Domain in der Dokumentation auf scanner.lucas-orth.de. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
186 lines
6.0 KiB
Markdown
186 lines
6.0 KiB
Markdown
# Deployment-Konventionen
|
|
|
|
Verbindlich fuer jede App, die auf dem VPS per Tag-Push deployt wird.
|
|
Abweichungen brauchen einen Grund, der hier nicht steht.
|
|
|
|
## Ablauf
|
|
|
|
```
|
|
git tag v1.2.3 && git push --tags
|
|
-> Gitea Actions (act_runner auf dem VPS)
|
|
-> npm ci / build / test <- rot bricht ab, nichts wird deployt
|
|
-> .env aus Repo-Secret DOTENV
|
|
-> docker compose up -d --build --wait
|
|
```
|
|
|
|
Kein SCP, kein manuelles Neubauen. Der Code landet nie im Dateisystem des
|
|
Hosts - der Job-Container klont, baut gegen den Docker-Daemon des Hosts und
|
|
startet den Container.
|
|
|
|
## Infrastruktur (existiert bereits, nicht neu anlegen)
|
|
|
|
| Ding | Wert |
|
|
|---|---|
|
|
| Git | Gitea, `https://gitea.lucas-orth.de`, Container `gitea`, intern `:3000` |
|
|
| Runner | `act_runner`, Container `gitea-runner`, Label `ubuntu-latest` |
|
|
| Netz CI | `gitea-ci` - Gitea + Runner + Job-Container |
|
|
| Netz Web | `nginx-proxy-manager_default` - Proxy + alle App-Container |
|
|
| Job-Image | `catthehacker/ubuntu:act-latest` (node + docker-cli + compose) |
|
|
|
|
Die beiden Netze sind bewusst getrennt: der Runner spricht mit Gitea, nicht
|
|
mit dem Proxy. App-Container haengen im Web-Netz, nicht im CI-Netz.
|
|
|
|
## docker-compose.yml
|
|
|
|
```yaml
|
|
# Fester Projektname. Ohne das leitet Compose ihn aus dem Verzeichnisnamen
|
|
# ab - und der ist im CI-Job ein anderer als auf dem Host.
|
|
name: business-card-scanner
|
|
|
|
services:
|
|
app:
|
|
# Versionstag ermoeglicht Rollback ohne Rebuild.
|
|
image: business-card-scanner:${IMAGE_TAG:-latest}
|
|
build: .
|
|
# Fester Name: das ist die Adresse, auf die der Proxy zeigt.
|
|
container_name: business-card-scanner
|
|
restart: unless-stopped
|
|
# Wird im Workflow aus dem Secret DOTENV erzeugt.
|
|
env_file: .env
|
|
networks:
|
|
- nginx-proxy-manager_default
|
|
healthcheck:
|
|
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8080/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
|
interval: 60s
|
|
timeout: 5s
|
|
retries: 3
|
|
|
|
networks:
|
|
nginx-proxy-manager_default:
|
|
external: true
|
|
|
|
volumes:
|
|
business-card-scanner-data:
|
|
# Fester Name, damit das Volume nicht am Projektnamen haengt.
|
|
name: business-card-scanner-data
|
|
```
|
|
|
|
**Kein `ports:`.** Der Proxy erreicht den Container ueber das gemeinsame Netz
|
|
unter seinem Container-Namen. Ein Port-Mapping wuerde den Dienst zusaetzlich
|
|
offen ans Internet haengen.
|
|
|
|
**Healthcheck ist Pflicht.** Der Deploy-Schritt nutzt `--wait` und macht ihn
|
|
zum Gate. Ohne Healthcheck wird der Lauf gruen, auch wenn der Container in
|
|
einer Crash-Schleife haengt.
|
|
|
|
**Keine relativen Bind-Mounts** (`./data:/data`). Compose laeuft im
|
|
Job-Container, der Pfad wuerde auf dem Host ins Leere zeigen. Persistente
|
|
Daten gehoeren in ein named volume mit festem `name:`.
|
|
|
|
## .gitea/workflows/deploy.yml
|
|
|
|
```yaml
|
|
name: Build & Deploy
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
# Intern klonen. Die oeffentliche Domain ist aus Containern heraus
|
|
# NICHT erreichbar: Gitea laeuft auf demselben Host, das Paket geht
|
|
# an die oeffentliche IP und findet nicht zurueck (NAT-Hairpin).
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
github-server-url: http://gitea:3000
|
|
|
|
# --- Build & Tests, projektspezifisch ---
|
|
|
|
- name: Tag ermitteln
|
|
run: echo "IMAGE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
|
|
|
- name: .env aus Secret erzeugen
|
|
env:
|
|
DOTENV: ${{ secrets.DOTENV }}
|
|
run: |
|
|
if [ -z "$DOTENV" ]; then
|
|
echo "Secret DOTENV ist leer oder nicht gesetzt."
|
|
exit 1
|
|
fi
|
|
umask 077
|
|
echo "$DOTENV" > .env
|
|
|
|
- name: Deployen
|
|
run: docker compose up -d --build --remove-orphans --wait --wait-timeout 180
|
|
|
|
- name: Alte Layer aufraeumen
|
|
run: docker image prune -f
|
|
```
|
|
|
|
Tests gehoeren **vor** den Deploy-Schritt. Ein roter Lauf darf den Server
|
|
nicht anfassen.
|
|
|
|
## Dockerfile
|
|
|
|
- Multi-Stage: Build-Abhaengigkeiten landen nicht im finalen Image.
|
|
- `USER node` bzw. non-root im finalen Stage.
|
|
- `EXPOSE 8080` dokumentiert den internen Port (oeffnet nichts).
|
|
- Ein Health-Endpoint (`/healthz`), auf den der Healthcheck zeigt.
|
|
|
|
## Secrets
|
|
|
|
Ein einziges Repo-Secret **`DOTENV`** mit dem kompletten `.env`-Inhalt,
|
|
mehrzeilig. Anlegen unter Repo -> Einstellungen -> Actions -> Secrets.
|
|
|
|
Gitea speichert Secrets write-only: nach dem Anlegen nicht mehr lesbar, nur
|
|
ersetzbar. Die kanonische Fassung gehoert deshalb in den Passwortmanager,
|
|
Gitea haelt nur die Arbeitskopie fuer die Pipeline.
|
|
|
|
Der Wert wird ueber `env:` an die Shell gereicht, nie direkt interpoliert -
|
|
sonst zerlegt der Shell-Parser Werte mit `$`, Backticks oder Quotes.
|
|
|
|
## Nginx Proxy Manager
|
|
|
|
Neuer Proxy Host:
|
|
|
|
| Feld | Wert |
|
|
|---|---|
|
|
| Domain Names | `scanner.lucas-orth.de` |
|
|
| Scheme | `http` |
|
|
| Forward Hostname / IP | `business-card-scanner` (= `container_name`) |
|
|
| Forward Port | `8080` |
|
|
|
|
Danach Let's-Encrypt-Zertifikat ausstellen und "Force SSL" aktivieren.
|
|
|
|
Weil der Container-Name die Adresse ist: aendert sich `container_name`,
|
|
zeigt der Proxy-Host ins Leere.
|
|
|
|
## Bestehende App umstellen
|
|
|
|
Fuer dieses Projekt nicht relevant - es lief nie manuell auf dem Server.
|
|
|
|
## Erstes Deployment einer App
|
|
|
|
1. Repo in Gitea anlegen, pushen.
|
|
2. Secret `DOTENV` setzen.
|
|
3. Pruefen, ob schon ein Container mit dem Namen laeuft
|
|
(`docker ps -a --filter name=business-card-scanner`). Falls ja: im alten Verzeichnis
|
|
einmal `docker compose down`, sonst kollidiert der feste Container-Name.
|
|
4. Proxy Host im NPM anlegen.
|
|
5. `git tag v1.0.0 && git push origin v1.0.0`
|
|
|
|
## Fallstricke
|
|
|
|
| Symptom | Ursache |
|
|
|---|---|
|
|
| Job haengt beim Checkout | `github-server-url` fehlt, klont ueber die oeffentliche Domain |
|
|
| `network not found` | App-Container im CI-Netz statt im Web-Netz (oder umgekehrt) |
|
|
| Zweiter Container statt Update | `name:` im Compose fehlt, Projektname weicht ab |
|
|
| Lauf gruen, App tot | `--wait` fehlt oder kein Healthcheck definiert |
|
|
| Volume leer nach Umstellung | Projektname geaendert, altes Volume hiess anders |
|
|
| Proxy liefert 502 | `container_name` geaendert oder Container nicht im Web-Netz |
|