diff --git a/.env.example b/.env.example index 685dbc6..42b9ac7 100644 --- a/.env.example +++ b/.env.example @@ -23,11 +23,13 @@ APP_URL="http://localhost:3000" # Port innerhalb des Containers bzw. im Standalone-Betrieb PORT="3000" -# Host-Port-Mapping fuer den App-Container (--profile full) -APP_PORT="3000" +# Host-Port-Mapping fuer den App-Container (--profile full). Nur auf dem +# Loopback – nach draussen geht es ueber einen Reverse Proxy. Soll die +# Instanz im LAN erreichbar sein, hier "3000" eintragen. +APP_PORT="127.0.0.1:3000" # ── Datenbank ──────────────────────────────────────────────── -# Standalone-Betrieb: App laeuft lokal, Postgres via "docker compose up -d db" +# Standalone-Betrieb: App laeuft lokal, Postgres via "npm run docker:db" # oder als nativ installierter Dienst. DATABASE_URL="postgresql://nebula:nebula@localhost:5432/nebula?schema=public" @@ -38,9 +40,10 @@ DATABASE_URL="postgresql://nebula:nebula@localhost:5432/nebula?schema=public" POSTGRES_USER="nebula" POSTGRES_PASSWORD="nebula" POSTGRES_DB="nebula" -# Host-Port, unter dem die Datenbank erreichbar ist. Nur die Entwicklung -# braucht ihn: im Container spricht die Anwendung "db:5432" an. Deshalb an -# 127.0.0.1 gebunden, damit er nicht im Netz steht. +# Host-Port der Datenbank. Wirkt NUR zusammen mit docker-compose.dev.yml +# (npm run docker:db) – die Basisdatei veroeffentlicht keinen DB-Port, weil +# die Anwendung im Container "db:5432" anspricht und nichts davon nach +# aussen muss. Auf einem Server bleibt dieser Wert also folgenlos. POSTGRES_PORT="127.0.0.1:5432" # Host-Port fuer Adminer (--profile tools) ADMINER_PORT="127.0.0.1:8080" diff --git a/CLAUDE.md b/CLAUDE.md index b0ea386..de9da75 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,8 @@ Betrieb: [README.md](README.md) · Server: [DEPLOYMENT.md](DEPLOYMENT.md) ## Womit man arbeitet ```bash -docker compose up -d db # Postgres, muss laufen +npm run docker:db # Postgres, muss laufen (mit dem dev-Overlay, + # sonst hat die DB keinen Host-Port) npm run dev # Entwicklung npm test # Unit-Tests (Vitest, ohne DB) npx tsc --noEmit # Typprüfung diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index ef8b05d..af1071a 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -62,7 +62,7 @@ chmod 600 .env openssl rand -base64 32 # den Wert als APP_SECRET eintragen ``` -Dann die `.env` öffnen und mindestens diese vier Werte anpassen: +Dann die `.env` öffnen und mindestens diese drei Werte anpassen: ```env # Die Adresse, unter der du Nebula im Browser aufrufst. @@ -74,11 +74,11 @@ APP_SECRET="…dein-langer-zufallswert…" # Das Datenbankpasswort. Die Voreinstellung „nebula" ist für Entwicklung. POSTGRES_PASSWORD="…noch-ein-langer-zufallswert…" - -# Nur lokal lauschen — nach außen geht es über den Reverse Proxy (Schritt 6). -APP_PORT=127.0.0.1:3000 ``` +Die Ports müssen nicht angefasst werden: `APP_PORT` steht schon auf +`127.0.0.1:3000`, und die Datenbank veröffentlicht gar nichts. + > **`APP_SECRET` später zu ändern** macht gespeicherte Git-Tokens unlesbar. > Sie müssen dann neu hinterlegt werden. Konten, Passwörter und Passkeys > bleiben unberührt. @@ -88,28 +88,34 @@ APP_PORT=127.0.0.1:3000 ### Welche Ports überhaupt hinausgehen -| Container | Host-Port | Wann | Wird gebraucht? | -| --- | --- | --- | --- | -| `nebula-app` | `APP_PORT` → 3000 | `--profile full` | ja — das ist die Anwendung | -| `nebula-db` | `POSTGRES_PORT` → 5432 | immer | **nein** | -| `nebula-adminer` | `ADMINER_PORT` → 8080 | nur `--profile tools` | nein | +Nach einer regulären Installation ist es **genau einer**: -**Der Datenbankport muss nicht nach außen.** Im Vollbetrieb spricht die -Anwendung Postgres über das interne Docker-Netz als `db:5432` an — die -`DATABASE_URL` setzt Compose selbst. Auch `scripts/backup.sh` und -`restore.sh` gehen über `docker exec`, nicht über den Host. Die Freigabe -existiert allein für die Entwicklung, wo `npm run dev` auf dem Host läuft. +| Container | Host-Port | Wann | +| --- | --- | --- | +| `nebula-app` | `APP_PORT`, Vorgabe `127.0.0.1:3000` | `--profile full` | +| `nebula-db` | — | **nie** | +| `nebula-adminer` | `ADMINER_PORT`, Vorgabe `127.0.0.1:8080` | nur `--profile tools` | -Sie ist deshalb auf `127.0.0.1` gebunden und damit vom Netz aus nicht -erreichbar. Auf einem Server kann sie ganz weg — siehe den nächsten -Abschnitt. +**Die Datenbank veröffentlicht nichts.** Im Vollbetrieb spricht die Anwendung +Postgres über das interne Docker-Netz als `db:5432` an — die `DATABASE_URL` +setzt Compose selbst. Auch `scripts/backup.sh` und `restore.sh` gehen über +`docker exec`, nicht über den Host. Ein Host-Port wäre nur eine zusätzliche +Angriffsfläche und würde mit anderen Postgres-Containern auf demselben Host +um den Port streiten. + +Wer die Datenbank beim Entwickeln doch vom Host aus braucht — `npm run dev` +läuft ja außerhalb des Containers — nimmt `docker-compose.dev.yml` dazu +(`npm run docker:db`). Auf einem Server hat diese Datei nichts zu suchen. + +Auch der Port der Anwendung liegt auf `127.0.0.1`: nach draußen geht es über +den Reverse Proxy aus Schritt 6. ### Variante: gar keinen Port veröffentlichen Liegen mehrere Stacks auf demselben Docker-Host und regelt ein gemeinsamer Reverse Proxy den Zugang, braucht Nebula überhaupt keinen Host-Port. Dafür -gibt es **`docker-compose.server.yml`**: es nimmt beide Freigaben zurück und -hängt die Anwendung zusätzlich in ein geteiltes Netz. +gibt es **`docker-compose.server.yml`**: es nimmt die letzte verbliebene +Freigabe zurück und hängt die Anwendung in ein geteiltes Netz. ```bash docker network create proxy # falls der Proxy-Stack es nicht schon anlegt @@ -240,9 +246,8 @@ sudo ufw allow 80,443/tcp sudo ufw enable ``` -Port 3000 und 5432 bleiben **zu**. Beide sind nach Schritt 3 ohnehin nur an -`127.0.0.1` gebunden — oder gar nicht erst veröffentlicht; die Firewall ist -der zweite Riegel. +Port 3000 bleibt **zu**, 5432 wird ohnehin nie veröffentlicht. Die Firewall +ist hier der zweite Riegel, nicht der erste. > Docker schreibt seine Portfreigaben direkt in `iptables` und geht dabei an > `ufw` **vorbei**. Eine Freigabe auf `0.0.0.0` steht also im Netz, auch wenn diff --git a/README.md b/README.md index 7eec54c..00fbee2 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ node scripts/setup.mjs # legt .env an und erzeugt ein zufälliges APP_SECRE Die App läuft nativ auf dem Rechner, nur PostgreSQL kommt aus dem Container: ```bash -docker compose up -d db # Postgres 17 auf localhost:5432 +npm run docker:db # Postgres 17 auf 127.0.0.1:5432 npm install npm run db:migrate # Schema anlegen npm run db:seed # optional: drei Beispielprojekte @@ -120,9 +120,9 @@ Alle Variablen sind in [`.env.example`](.env.example) dokumentiert. | `APP_SECRET` | Schlüssel für die Token-Verschlüsselung (AES-256-GCM) | — **unbedingt setzen** | | `SESSION_TTL_DAYS` | Lebensdauer einer Anmeldung in Tagen | `30` | | `PORT` | Port der Anwendung | `3000` | -| `APP_PORT` | Host-Port-Mapping des App-Containers | `3000` | +| `APP_PORT` | Host-Port-Mapping des App-Containers | `127.0.0.1:3000` | | `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` | Zugangsdaten des DB-Containers | `nebula` | -| `POSTGRES_PORT` | Host-Port der Datenbank — nur für die Entwicklung, im Container läuft es über `db:5432` | `127.0.0.1:5432` | +| `POSTGRES_PORT` | Host-Port der Datenbank — wirkt nur mit `docker-compose.dev.yml`, sonst hat die DB keinen | `127.0.0.1:5432` | | `ADMINER_PORT` | Host-Port von Adminer (`--profile tools`) | `127.0.0.1:8080` | | `GIT_API_TIMEOUT_MS` | Timeout für Git-API-Aufrufe | `15000` | | `GIT_ALLOW_LOOPBACK` | Git-Instanz auf demselben Rechner erlauben | `false` | @@ -307,7 +307,7 @@ nebula/ | `npm test` | Unit-Tests (Vitest, ohne Datenbank) | | `npm run test:e2e` | Ende-zu-Ende-Tests (Playwright, baut vorher) | | `npm run test:all` | Beides nacheinander | -| `npm run docker:db` | Nur die Datenbank starten | +| `npm run docker:db` | Nur die Datenbank starten, mit Host-Port für `npm run dev` | | `npm run docker:up` / `docker:down` | Kompletten Stack starten / stoppen | | `npm run docker:tools` | Adminer auf `http://localhost:8080` | @@ -416,7 +416,7 @@ Grenzen, die du kennen solltest: | Symptom | Ursache / Lösung | | --- | --- | -| `Can't reach database server` | Läuft `docker compose up -d db`? Stimmt `DATABASE_URL`? | +| `Can't reach database server` | Läuft `npm run docker:db`? Ein blosses `docker compose up -d db` startet die DB **ohne Host-Port** — dann ist sie von `npm run dev` aus nicht erreichbar. Stimmt `DATABASE_URL`? | | `The table ... does not exist` | Migrationen fehlen: `npm run db:migrate` | | Sync meldet `401` | Token abgelaufen oder falsche Scopes — unter Git-Verbindungen neu setzen | | Sync meldet `404` | Owner/Repo prüfen; bei GitLab gehört die volle Gruppenkette in „Owner" | diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..93b094a --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,23 @@ +# ───────────────────────────────────────────────────────────── +# Overlay fuer die Entwicklung: macht die Datenbank auf dem Host erreichbar. +# +# Gebraucht wird das nur, wenn die Anwendung ausserhalb des Containers +# laeuft – "npm run dev" verbindet sich dann ueber localhost:5432. Im +# Vollbetrieb spricht die Anwendung "db:5432" im Docker-Netz an und braucht +# diese Datei nicht. +# +# Aufruf: +# docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d db +# npm run dev +# +# Kuerzer: npm run docker:db +# +# Gebunden wird auf 127.0.0.1, nicht auf 0.0.0.0. Docker traegt Freigaben +# direkt in iptables ein und umgeht dabei ufw – eine offene Bindung stuende +# also auch bei aktiver Firewall im Netz. +# ───────────────────────────────────────────────────────────── + +services: + db: + ports: + - "${POSTGRES_PORT:-127.0.0.1:5432}:5432" diff --git a/docker-compose.server.yml b/docker-compose.server.yml index 6b86131..e1157d1 100644 --- a/docker-compose.server.yml +++ b/docker-compose.server.yml @@ -1,10 +1,11 @@ # ───────────────────────────────────────────────────────────── -# Overlay fuer den Serverbetrieb: kein Port geht nach aussen. +# Overlay fuer den Serverbetrieb: kein einziger Port geht zum Host. # # Gedacht fuer einen Docker-Host, auf dem mehrere Stacks liegen und ein -# gemeinsamer Reverse Proxy die einzige Tuer nach draussen ist. Weder die -# Datenbank noch die Anwendung veroeffentlichen dann einen Host-Port; der -# Proxy erreicht Nebula ueber ein geteiltes Docker-Netz. +# gemeinsamer Reverse Proxy die einzige Tuer nach draussen ist. Die +# Datenbank veroeffentlicht ohnehin nichts; diese Datei nimmt zusaetzlich +# die Freigabe der Anwendung zurueck und haengt sie in ein geteiltes Netz, +# ueber das der Proxy sie erreicht. # # Aufruf: # docker compose -f docker-compose.yml -f docker-compose.server.yml \ @@ -14,6 +15,7 @@ # COMPOSE_FILE=docker-compose.yml:docker-compose.server.yml # COMPOSE_PROFILES=full # Danach genuegt ueberall "docker compose up -d" – auch in update.sh. +# (Trennzeichen: Doppelpunkt unter Linux, Semikolon unter Windows.) # # Das Netz muss es vorher geben (der Proxy-Stack legt es ueblicherweise an): # docker network create proxy @@ -22,13 +24,11 @@ # ───────────────────────────────────────────────────────────── services: - db: + app: # !reset loescht die Portfreigabe aus der Basisdatei. Ohne dieses Tag # wuerden die Listen zusammengefuegt statt ersetzt – Compose behandelt - # "ports" beim Zusammenfuehren als anzuhaengende Liste. - ports: !reset [] - - app: + # "ports" beim Zusammenfuehren als anzuhaengende Liste. Braucht + # Compose 2.24 oder neuer. ports: !reset [] networks: # Das stackeigene Netz: darueber spricht die Anwendung mit der Datenbank. diff --git a/docker-compose.yml b/docker-compose.yml index 3642616..0a1149c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,20 @@ name: nebula # ───────────────────────────────────────────────────────────── -# Zwei Betriebsarten aus einer Datei: +# Diese Datei veroeffentlicht nur einen einzigen Port, und den auf dem +# Loopback: den der Anwendung. Die Datenbank bleibt im Docker-Netz. # -# 1) Standalone-Entwicklung – nur die Datenbank im Container: -# docker compose up -d db +# 1) Alles im Container (Server wie Entwicklung): +# docker compose --profile full up -d --build +# +# 2) Standalone-Entwicklung – Datenbank im Container, App auf dem Host. +# Dafuer muss Postgres auf dem Host erreichbar sein, also mit dem +# Entwicklungs-Overlay (oder kurz: npm run docker:db): +# docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d db # npm run dev # -# 2) Alles im Container: -# docker compose --profile full up -d --build +# 3) Server ohne jede Portfreigabe, hinter einem Reverse Proxy im +# gemeinsamen Docker-Netz: siehe docker-compose.server.yml # # Optionales DB-Webinterface: # docker compose --profile tools up -d adminer → http://localhost:8080 @@ -25,12 +31,10 @@ services: POSTGRES_DB: ${POSTGRES_DB:-nebula} # Deterministische Sortierung unabhaengig vom Host-Locale POSTGRES_INITDB_ARGS: "--encoding=UTF8" - ports: - # Nur fuer die Entwicklung – die Anwendung erreicht die Datenbank im - # Container ueber "db:5432", nicht ueber den Host. Deshalb an 127.0.0.1 - # gebunden. Im Serverbetrieb faellt die Freigabe ganz weg, siehe - # docker-compose.server.yml. - - "${POSTGRES_PORT:-127.0.0.1:5432}:5432" + # Absichtlich keine Portfreigabe: die Anwendung erreicht die Datenbank im + # Container ueber "db:5432", und backup.sh geht ueber "docker exec". Nach + # aussen muss hier nichts. Wer die Datenbank vom Host aus braucht – fuer + # "npm run dev" oder psql – nimmt docker-compose.dev.yml dazu. volumes: - nebula-db-data:/var/lib/postgresql/data healthcheck: @@ -77,7 +81,11 @@ services: PORT: 3000 HOSTNAME: 0.0.0.0 ports: - - "${APP_PORT:-3000}:3000" + # Nur auf dem Loopback: nach draussen kommt man ueber den Reverse Proxy. + # Docker schreibt seine Freigaben an ufw vorbei direkt in iptables – eine + # Bindung auf 0.0.0.0 stuende also im Netz, auch bei aktiver Firewall. + # Ganz ohne Host-Port: docker-compose.server.yml. + - "${APP_PORT:-127.0.0.1:3000}:3000" adminer: profiles: ["tools"] diff --git a/package.json b/package.json index 6190e68..158a59c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "db:studio": "prisma studio", "db:seed": "prisma db seed", "db:reset": "prisma migrate reset", - "docker:db": "docker compose up -d db", + "docker:db": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d db", "docker:db:down": "docker compose stop db", "docker:up": "docker compose --profile full up -d --build", "docker:down": "docker compose --profile full down",