Files
beispiel-app/.vch/PLATFORM.md
T
2026-09-02 20:44:16 +00:00

15 KiB
Raw Blame History

VCH-Plattform — die Regeln dieser Umgebung

Diese Datei gehört VCH und wird bei jedem Abgleich überschrieben. Projektwissen gehört in die CLAUDE.md daneben, nicht hierher.

Sie beschreibt, was VCH mit diesem Projekt macht — vor allem die Stellen, an denen ein Verstoß nicht als Fehler auffällt, sondern als falsches Ergebnis: ein Deploy, der grün meldet und nichts ausliefert, eine Datenbank, die niemand verbindet, ein Audit, das etwas prüft, das du gar nicht gepusht hast.

1. Zwei Maschinen, nicht eine

  • Hier (DEV): ein Debian-LXC. Nutzer vchuser, Heimat /home/vchuser, Projekte unter /home/vchuser/projects/<name>. code-server auf Port 8080, Claude Code als claude, Git gegen Gitea vorkonfiguriert.
  • Dort (PROD): eine andere Maschine. VCH klont dein Repository dorthin und startet es.

Daraus folgt die wichtigste Regel überhaupt: VCH deployt und auditiert deinen gepushten Commit, nie dein Arbeitsverzeichnis. Nicht committete Änderungen existieren für Deploy, Audit und Veröffentlichung nicht — auch dann nicht, wenn hier alles läuft.

2. Vorschau im Browser läuft über einen Proxy

Deine Entwicklungs-Server sind nicht direkt erreichbar. VCH erkennt lauschende Ports und stellt zwei Wege bereit:

  1. Subdomain (bevorzugt): https://lxc<VMID>-<PORT>.<domain>/ — deine App liegt auf der Wurzel, alles verhält sich normal.
  2. Pfad-Proxy (Rückfallweg): https://<host>/proxy/<instanz>/p/<port>/ — deine App liegt hinter einem Pfadpräfix.

Damit beides funktioniert: relative Pfade verwenden.

  • css/style.css ✅ — nicht /css/style.css ❌
  • fetch('api/daten') ✅ — nicht fetch('/api/daten') ❌
  • Wenn absolute Pfade unvermeidbar sind: <base href="./"> in den <head>.

Rahmenwerke: Vite base: './' · Create React App "homepage": "." in der package.json · Next.js Vorgabe belassen (Subdomain) oder basePath setzen (Pfad-Proxy) · Express express.static genügt, solange das HTML relativ verweist.

3. Dateien, die VCH im Projektstamm liest

Datei Pflicht Wozu
README.md ja Audit-Prüfung readme — blockiert die Veröffentlichung
.vch-description ja Audit-Prüfung description; der Text steht später im Showcase
.env.example wenn die App Umgebungsvariablen liest die Deklaration aller Variablen (Abschnitt 6)
.vch/deploy.yaml wenn die Autoerkennung nicht reicht wie gebaut und gestartet wird (Abschnitt 5)
CLAUDE.md — dein Projektwissen; VCH fasst sie nicht mehr an

README.md muss beschreiben, was das Projekt tut, wie man es installiert, wie man es startet und worauf es aufbaut. Die unveränderte Vorlage besteht die Prüfung nicht.

.vch-description ist nicht technisch: was das Projekt tut, welches Problem es löst, für wen. Kein Technologie-Stapel, keine Installationsanleitung — die stehen im README. Halte beide aktuell, wenn sich Zweck oder Aufbau ändern.

4. Der Port gehört VCH

VCH weist jedem Projekt auf der PROD-Maschine einen festen Port zu (Bereich 13000–13999) und hält ihn über alle Deploys hinweg stabil. Du wählst ihn nicht und stimmst dich mit niemandem ab.

Deine App MUSS auf PORT aus der Umgebung lauschen:

  • Node/Express: app.listen(process.env.PORT || 3000)
  • Next.js: next start beachtet PORT von selbst
  • Python: gunicorn app:app -b 0.0.0.0:$PORT bzw. uvicorn main:app --port $PORT
  • Go/andere: den Wert aus der Umgebung lesen, nicht hartkodieren

Ein fest verdrahteter Port läuft hier und scheitert dort am Healthcheck. PORT und VCH_DATA_DIR sind VCH-eigene Schlüssel: setzt du sie selbst — in .env.example, im Env-Editor oder im Compose — wird dein Wert verworfen.

5. .vch/deploy.yaml — die vollständige Referenz

Ohne diese Datei versucht VCH, das Projekt selbst zu erkennen (Abschnitt 5.3). Reicht das nicht, bricht der Deploy ab und legt dir eine fertige Vorlage vor, die du nur noch committen musst.

5.1 Alle Felder

Feld Typ Vorgabe Bedeutung
runtime auto | systemd | docker auto Wie gestartet wird. auto: Dockerfile oder Compose vorhanden → docker, sonst systemd.
build Zeichenkette — Shell-Befehl im Release-Verzeichnis. Nur systemd — bei docker baut das Dockerfile.
start Zeichenkette — Startbefehl des Dienstes. Nur systemd. Muss $PORT binden.
port Ganzzahl 1–65535 3000 Nur docker: der Port, auf dem deine App im Container lauscht. VCH bildet den zugewiesenen Host-Port darauf ab.
health Pfad / Pfad, der 2xx oder 3xx liefert, sobald die App bereit ist.
env Liste von Zeichenketten [] Zusätzliche Variablennamen, die VCH ausliefern soll. Wird mit den Schlüsseln aus .env.example vereinigt.
compose Pfad — Welche Compose-Datei gilt, wenn mehrere gefunden werden.

Unbekannte Felder und Tippfehler in runtime brechen den Deploy mit einer benannten Fehlermeldung ab — sie werden nicht still ignoriert.

5.2 Beispiele

Node mit eigenem Build (systemd):

runtime: systemd
build: "npm ci && npm run build"
start: "npm start"
health: /

Python (systemd) — beachte $PORT im Startbefehl:

runtime: systemd
build: "python3 -m venv .venv && .venv/bin/pip install -r requirements.txt"
start: ".venv/bin/gunicorn app:app -b 0.0.0.0:$PORT"
health: /

Statische Seite (systemd) — irgendetwas muss lauschen:

runtime: systemd
build: "npm ci && npm run build"
start: "python3 -m http.server $PORT --directory dist"

Monorepo, Code in einem Unterordner:

runtime: systemd
build: "cd backend && npm ci && npm run build"
start: "cd backend && npm start"

Dockerfile:

runtime: docker
port: 3000        # der Port INNERHALB des Containers
health: /

Mehrere Compose-Dateien im Repository:

runtime: docker
compose: docker/compose.yaml

5.3 Was ohne die Datei passiert

In dieser Reihenfolge:

  1. Ein echtes Dockerfile gewinnt → runtime: docker mit dem Image.
  2. Eine Compose-Datei, die ${VCH_PORT} veröffentlicht → Compose-Deploy.
  3. Eine Compose-Datei, die nur Infrastruktur startet (Postgres, Redis, … und keinen App-Dienst), wird als Entwicklungs-Beiwerk erkannt und nicht deployt — VCH sucht weiter nach der Sprache.
  4. Eine Compose-Datei mit App-Dienst, die ${VCH_PORT} nicht veröffentlicht → Abbruch mit Vorlage (Abschnitt 10).
  5. package.json im Stamm: build-Skript → Build, start-Skript bzw. main → Start. Paketmanager wird am Lockfile erkannt (pnpm/yarn/npm).
  6. Alles andere — Python, PHP/Laravel, Go, reines Frontend ohne start, Code nur in Unterordnern — endet mit einer passenden Vorlage zum Committen.

6. Umgebungsvariablen: .env.example ist die Deklaration

VCH liest keine Umgebungsvariablen aus deinem Code. Was in einer Beispieldatei steht, existiert; alles andere nicht.

  • Erkannte Dateinamen: .env.example, .env.sample, .env.template sowie dieselben ohne führenden Punkt (env.example, …). Der erste Fund je Schlüssel gewinnt.
  • Gesucht wird im ganzen Repository (ohne node_modules, .git, dist, build, .next, out, vendor, coverage, .turbo), Stammdateien zuerst, höchstens 25 Dateien.
  • Jede gefundene Beispieldatei bestimmt ein Ziel: backend/.env.example → VCH schreibt die Produktionswerte nach backend/.env. Für Monorepos ist das der Weg, mehrere .env zu bekommen.

Was den Deploy blockiert: ein Schlüssel, der keinen Wert nennt (KEY=) und für den noch kein Produktionswert hinterlegt ist. Nicht blockierend sind:

API_URL=https://beispiel.test     # Vorgabewert vorhanden
OPTIONAL_TOKEN=""                 # ausdrücklich leer erlaubt
FEATURE_FLAG=            # optional

Der Marker muss direkt hinter dem # stehen (# optional); Fließtext, in dem das Wort vorkommt, zählt nicht. Die Vorgabewerte sind Hinweise für den Menschen — in Produktion wird nichts davon verwendet.

Im Zweifel:

grep -RhoE "process\.env\.[A-Z_]+" src/ | sort -u

und jeden Treffer in die .env.example schreiben.

Bei Docker: nur Variablen mit dem Präfix NEXT_PUBLIC_, VITE_ oder PUBLIC_ gehen als --build-arg in den Build (sie sind ohnehin im Auslieferungsstand sichtbar) — dafür brauchst du passende ARG-Zeilen im Dockerfile. Alle übrigen Werte kommen erst zur Laufzeit über --env-file und landen nie im Image.

7. Datenbank

Deklariert deine App Verbindungsvariablen, bietet VCH eine verwaltete Datenbank an und trägt die Werte automatisch in genau die Schlüssel ein, die du deklariert hast. Du musst deine Variablennamen also nicht ändern — VCH erkennt sie.

Erkannt wird (Groß-/Kleinschreibung egal):

  • Verbindungszeichenketten: DATABASE_URL, DB_URL, jeder Name auf _DSN
  • Einzelteile mit Familienwort (DB, DATABASE, POSTGRES, MYSQL) plus Rolle: ..._HOST, ..._PORT, ..._USER/..._USERNAME, ..._PASSWORD/..._PASS, ..._NAME/..._DATABASE/..._DB
  • Die üblichen nackten Namen: PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE, MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, POSTGRES_DB

Bewusst nicht erkannt (und deshalb nie überschrieben):

  • alles mit ROOT im Namen (POSTGRES_ROOT_PASSWORD, MYSQL_ROOT_PASSWORD) — das sind die Zugangsdaten des Datenbankservers, nicht die deiner App
  • SENTRY_DSN und verwandte Fehlermelder (GLITCHTIP, BUGSNAG, ROLLBAR, AIRBRAKE, RAYGUN, APPSIGNAL) — ein DSN ist dort keine Datenbank
  • HISTORY_DB=/data/verlauf.sqlite und Ähnliches: ein ..._DB ohne Datenbank-Familienwort im übrigen Namen gilt nicht als Verbindung

Die Engine (Postgres oder MySQL) leitet VCH aus deinen Schlüsselnamen und dem Schema in DATABASE_URL ab; ist das mehrdeutig, entscheidest du im Projektarbeitsplatz.

Bringt dein Compose seine eigene Datenbank mit, startet VCH sie mit und bietet keine zweite an. Achte dann selbst auf ein dauerhaftes Volume — ein Datenbankdienst ohne Volume verliert seine Daten beim nächsten Neubau, und VCH warnt nur.

Eine Compose-Datei, die nur eine Datenbank startet, ist Entwicklungs-Beiwerk: sie läuft in Produktion nicht mit. Nimm dort die verwaltete Datenbank.

8. Dauerhafte Daten: VCH_DATA_DIR

Jedes Release liegt in einem eigenen Verzeichnis und verschwindet wieder. Was überleben soll — SQLite-Datei, Uploads, erzeugte Dateien — gehört in das Verzeichnis aus VCH_DATA_DIR.

const datenVerzeichnis = process.env.VCH_DATA_DIR || './data';

VCH setzt die Variable bei allen Betriebsarten (bei Docker als Einhängepunkt /data). Lies sie zur Laufzeit, schreibe den Pfad nirgends fest und trage sie nicht in .env-Dateien ein. Ein Verzeichnis relativ zum Release (./data) ist beim nächsten Deploy weg.

Der Prüfling während des Audits (Abschnitt 9) bekommt diesen Speicher nicht — er läuft gegen einen leeren. Das ist Absicht: zwei Prozesse dürfen nicht gleichzeitig auf dieselbe SQLite-Datei schreiben.

9. Wie ein Deploy abläuft

Klonen → Bauen → Healthcheck (Prüfling) → Audit → Umschalten → Live
  1. Klonen. VCH klont den Deploy-Branch auf die PROD-Maschine — genau den gepushten Stand.
  2. Torprüfung. Fehlt ein blockierender Wert aus .env.example (Abschnitt 6), endet es hier, bevor irgendetwas läuft.
  3. Bauen nach .vch/deploy.yaml bzw. Autoerkennung.
  4. Healthcheck. Der neue Stand startet als Prüfling auf einem freien Port aus dem Bereich 14000–14999, neben der weiterhin laufenden alten Fassung. VCH ruft dort deinen health-Pfad auf: rund 30 Sekunden lang, alle 1,5 s ein Versuch, Erfolg beim ersten 2xx/3xx. Bis dahin muss die App antworten.
  5. Audit (Abschnitt 10) gegen den deployten Commit.
  6. Umschalten. Erst wenn das Audit besteht, wird der Prüfling zur laufenden Fassung auf dem stabilen Port. Ein durchgefallenes Audit heißt: der alte Stand bleibt live.

Vor dem Umschalten legt VCH einen Datenschnappschuss an. Geht das Umschalten schief, werden erst die Daten und dann der Code zurückgerollt. Frühere Releases lassen sich im Projektarbeitsplatz per Rollback zurückholen.

Auto-Deploy: ist er eingeschaltet, löst jeder Push auf den konfigurierten Deploy-Branch einen Deploy aus. Pushes auf andere Branches werden ignoriert.

10. Audit — was geprüft wird

Das Audit läuft nicht hier, sondern in einem eigenen Runner gegen einen sauberen Checkout des deployten Commits. Nicht Gepushtes wird nicht geprüft — und ein .gitignore-Eintrag macht eine Datei für das Audit unsichtbar.

Prüfung Schwere Darf entfallen
readme Fehler nein
secrets Fehler nein
description Warnung nein
lint Fehler ja
typescript Fehler ja
npm-audit Fehler ja
  • Eine Warnung blockiert nie. description hält dich also nicht auf, kostet dich aber den Platz im Showcase.
  • „Darf entfallen" heißt: die Prüfung meldet skipped, wenn sie auf dein Projekt nicht passt (kein TypeScript, kein npm). Eine Prüfung, die nicht entfallen darf, aber trotzdem skipped meldet, gilt als durchgefallen — sie ist nie gelaufen.
  • secrets sucht eingecheckte Zugangsdaten. Echte Werte gehören nie ins Repository: in die .env.example nur Beispiele, die echten Werte in den Env-Editor von VCH.

11. Veröffentlichen

  • Intern erreichbar ist dein Projekt, sobald ein Deploy läuft — ohne Audit, ohne weiteren Schritt, unter http://<prod-ip>:<port>.
  • Öffentlich unter einer Subdomain mit SSL wird es erst mit laufendem Deploy und bestandenem Audit für genau diesen Deploy. Die Subdomain kommt oben drauf; entfernst du sie wieder, bleibt das Projekt intern erreichbar.
  • Im Showcase erscheinen veröffentlichte Projekte mit bestandenem Audit und einer eingecheckten .vch-description.

12. Compose — die vier Regeln, an denen es sonst scheitert

  1. Der App-Dienst muss seinen Port als ports: ["${VCH_PORT}:<intern>"] veröffentlichen. VCH setzt VCH_PORT und kann so einen Prüfling neben deiner laufenden Fassung starten. Ohne diese Zeile bricht der Deploy ab.
  2. VCH führt genau EINE Compose-Datei aus — keine -f-Ketten. Gesucht wird compose.yaml/compose.yml/docker-compose.yaml/docker-compose.yml im Stamm oder in einem Unterordner. Eine Override-Datei (docker-compose.ports.yml, *.override.yml) wird ignoriert — die ${VCH_PORT}-Zeile gehört in die Hauptdatei. Bei mehreren Fundstellen: compose: <pfad> in die .vch/deploy.yaml.
  3. Jeder Dienst, der Produktionswerte braucht, bekommt env_file: [.env]. VCH schreibt deine Werte in eine .env neben der Compose-Datei.
  4. Der ganze Stapel startet. Datenbanken, Caches und Ähnliches aus deinem Compose laufen automatisch mit — eine zusätzliche verwaltete Datenbank brauchst du dafür nicht. Dienste hinter einem profiles:-Schlüssel bleiben aus.