Files
business-card-scanner/DEPLOY.md
Lucas Orth 1f29d5eb27
All checks were successful
Build & Deploy / deploy (push) Successful in 1m14s
effort-Parameter modellabhängig senden, Design-Hierarchie, iOS-Datumsfeld
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>
2026-09-06 12:36:24 +02:00

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 |