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

6.0 KiB

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

# 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

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