fix(docker): die Datenbank veroeffentlicht ueberhaupt keinen Host-Port mehr
CI / pruefen (push) Has been cancelled

Der letzte Anlauf hat das Problem nicht geloest. Die Freigabe stand weiter in
der Basisdatei, nur mit anderem Vorgabewert -- und Vorgabewerte greifen nicht,
wo die Variable schon gesetzt ist: in jeder .env, die aus der bisherigen
.env.example entstanden ist, steht POSTGRES_PORT="5432". Postgres band also
unveraendert auf 0.0.0.0 und kollidierte mit anderen Stacks auf demselben
Host; der Container kam gar nicht erst hoch. Das Overlay half nicht, weil es
erst greift, wenn man es dazunimmt.

Jetzt hat der db-Dienst keinen ports-Block mehr. Gebraucht wird er nur, wenn
die Anwendung ausserhalb des Containers laeuft, und dafuer gibt es
docker-compose.dev.yml (npm run docker:db). Damit ist der sichere Fall die
Vorgabe und der Sonderfall die Ausnahme, nicht umgekehrt.

Der App-Port bindet aus demselben Grund auf 127.0.0.1: Docker traegt seine
Freigaben direkt in iptables ein und umgeht dabei ufw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 09:46:24 +02:00
co-authored by Claude Opus 5
parent d311fe48c0
commit fc54c73791
8 changed files with 96 additions and 56 deletions
+9 -6
View File
@@ -23,11 +23,13 @@ APP_URL="http://localhost:3000"
# Port innerhalb des Containers bzw. im Standalone-Betrieb # Port innerhalb des Containers bzw. im Standalone-Betrieb
PORT="3000" PORT="3000"
# Host-Port-Mapping fuer den App-Container (--profile full) # Host-Port-Mapping fuer den App-Container (--profile full). Nur auf dem
APP_PORT="3000" # 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 ──────────────────────────────────────────────── # ── 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. # oder als nativ installierter Dienst.
DATABASE_URL="postgresql://nebula:nebula@localhost:5432/nebula?schema=public" 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_USER="nebula"
POSTGRES_PASSWORD="nebula" POSTGRES_PASSWORD="nebula"
POSTGRES_DB="nebula" POSTGRES_DB="nebula"
# Host-Port, unter dem die Datenbank erreichbar ist. Nur die Entwicklung # Host-Port der Datenbank. Wirkt NUR zusammen mit docker-compose.dev.yml
# braucht ihn: im Container spricht die Anwendung "db:5432" an. Deshalb an # (npm run docker:db) – die Basisdatei veroeffentlicht keinen DB-Port, weil
# 127.0.0.1 gebunden, damit er nicht im Netz steht. # 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" POSTGRES_PORT="127.0.0.1:5432"
# Host-Port fuer Adminer (--profile tools) # Host-Port fuer Adminer (--profile tools)
ADMINER_PORT="127.0.0.1:8080" ADMINER_PORT="127.0.0.1:8080"
+2 -1
View File
@@ -9,7 +9,8 @@ Betrieb: [README.md](README.md) · Server: [DEPLOYMENT.md](DEPLOYMENT.md)
## Womit man arbeitet ## Womit man arbeitet
```bash ```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 run dev # Entwicklung
npm test # Unit-Tests (Vitest, ohne DB) npm test # Unit-Tests (Vitest, ohne DB)
npx tsc --noEmit # Typprüfung npx tsc --noEmit # Typprüfung
+27 -22
View File
@@ -62,7 +62,7 @@ chmod 600 .env
openssl rand -base64 32 # den Wert als APP_SECRET eintragen 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 ```env
# Die Adresse, unter der du Nebula im Browser aufrufst. # 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. # Das Datenbankpasswort. Die Voreinstellung „nebula" ist für Entwicklung.
POSTGRES_PASSWORD="…noch-ein-langer-zufallswert…" 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. > **`APP_SECRET` später zu ändern** macht gespeicherte Git-Tokens unlesbar.
> Sie müssen dann neu hinterlegt werden. Konten, Passwörter und Passkeys > Sie müssen dann neu hinterlegt werden. Konten, Passwörter und Passkeys
> bleiben unberührt. > bleiben unberührt.
@@ -88,28 +88,34 @@ APP_PORT=127.0.0.1:3000
### Welche Ports überhaupt hinausgehen ### Welche Ports überhaupt hinausgehen
| Container | Host-Port | Wann | Wird gebraucht? | Nach einer regulären Installation ist es **genau einer**:
| --- | --- | --- | --- |
| `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 |
**Der Datenbankport muss nicht nach außen.** Im Vollbetrieb spricht die | Container | Host-Port | Wann |
Anwendung Postgres über das interne Docker-Netz als `db:5432` an — die | --- | --- | --- |
`DATABASE_URL` setzt Compose selbst. Auch `scripts/backup.sh` und | `nebula-app` | `APP_PORT`, Vorgabe `127.0.0.1:3000` | `--profile full` |
`restore.sh` gehen über `docker exec`, nicht über den Host. Die Freigabe | `nebula-db` | — | **nie** |
existiert allein für die Entwicklung, wo `npm run dev` auf dem Host läuft. | `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 **Die Datenbank veröffentlicht nichts.** Im Vollbetrieb spricht die Anwendung
erreichbar. Auf einem Server kann sie ganz weg — siehe den nächsten Postgres über das interne Docker-Netz als `db:5432` an — die `DATABASE_URL`
Abschnitt. 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 ### Variante: gar keinen Port veröffentlichen
Liegen mehrere Stacks auf demselben Docker-Host und regelt ein gemeinsamer Liegen mehrere Stacks auf demselben Docker-Host und regelt ein gemeinsamer
Reverse Proxy den Zugang, braucht Nebula überhaupt keinen Host-Port. Dafür 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 gibt es **`docker-compose.server.yml`**: es nimmt die letzte verbliebene
hängt die Anwendung zusätzlich in ein geteiltes Netz. Freigabe zurück und hängt die Anwendung in ein geteiltes Netz.
```bash ```bash
docker network create proxy # falls der Proxy-Stack es nicht schon anlegt 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 sudo ufw enable
``` ```
Port 3000 und 5432 bleiben **zu**. Beide sind nach Schritt 3 ohnehin nur an Port 3000 bleibt **zu**, 5432 wird ohnehin nie veröffentlicht. Die Firewall
`127.0.0.1` gebunden — oder gar nicht erst veröffentlicht; die Firewall ist ist hier der zweite Riegel, nicht der erste.
der zweite Riegel.
> Docker schreibt seine Portfreigaben direkt in `iptables` und geht dabei an > 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 > `ufw` **vorbei**. Eine Freigabe auf `0.0.0.0` steht also im Netz, auch wenn
+5 -5
View File
@@ -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: Die App läuft nativ auf dem Rechner, nur PostgreSQL kommt aus dem Container:
```bash ```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 install
npm run db:migrate # Schema anlegen npm run db:migrate # Schema anlegen
npm run db:seed # optional: drei Beispielprojekte 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** | | `APP_SECRET` | Schlüssel für die Token-Verschlüsselung (AES-256-GCM) | — **unbedingt setzen** |
| `SESSION_TTL_DAYS` | Lebensdauer einer Anmeldung in Tagen | `30` | | `SESSION_TTL_DAYS` | Lebensdauer einer Anmeldung in Tagen | `30` |
| `PORT` | Port der Anwendung | `3000` | | `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_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` | | `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_API_TIMEOUT_MS` | Timeout für Git-API-Aufrufe | `15000` |
| `GIT_ALLOW_LOOPBACK` | Git-Instanz auf demselben Rechner erlauben | `false` | | `GIT_ALLOW_LOOPBACK` | Git-Instanz auf demselben Rechner erlauben | `false` |
@@ -307,7 +307,7 @@ nebula/
| `npm test` | Unit-Tests (Vitest, ohne Datenbank) | | `npm test` | Unit-Tests (Vitest, ohne Datenbank) |
| `npm run test:e2e` | Ende-zu-Ende-Tests (Playwright, baut vorher) | | `npm run test:e2e` | Ende-zu-Ende-Tests (Playwright, baut vorher) |
| `npm run test:all` | Beides nacheinander | | `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:up` / `docker:down` | Kompletten Stack starten / stoppen |
| `npm run docker:tools` | Adminer auf `http://localhost:8080` | | `npm run docker:tools` | Adminer auf `http://localhost:8080` |
@@ -416,7 +416,7 @@ Grenzen, die du kennen solltest:
| Symptom | Ursache / Lösung | | 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` | | `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 `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" | | Sync meldet `404` | Owner/Repo prüfen; bei GitLab gehört die volle Gruppenkette in „Owner" |
+23
View File
@@ -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"
+9 -9
View File
@@ -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 # Gedacht fuer einen Docker-Host, auf dem mehrere Stacks liegen und ein
# gemeinsamer Reverse Proxy die einzige Tuer nach draussen ist. Weder die # gemeinsamer Reverse Proxy die einzige Tuer nach draussen ist. Die
# Datenbank noch die Anwendung veroeffentlichen dann einen Host-Port; der # Datenbank veroeffentlicht ohnehin nichts; diese Datei nimmt zusaetzlich
# Proxy erreicht Nebula ueber ein geteiltes Docker-Netz. # die Freigabe der Anwendung zurueck und haengt sie in ein geteiltes Netz,
# ueber das der Proxy sie erreicht.
# #
# Aufruf: # Aufruf:
# docker compose -f docker-compose.yml -f docker-compose.server.yml \ # 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_FILE=docker-compose.yml:docker-compose.server.yml
# COMPOSE_PROFILES=full # COMPOSE_PROFILES=full
# Danach genuegt ueberall "docker compose up -d" – auch in update.sh. # 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): # Das Netz muss es vorher geben (der Proxy-Stack legt es ueblicherweise an):
# docker network create proxy # docker network create proxy
@@ -22,13 +24,11 @@
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
services: services:
db: app:
# !reset loescht die Portfreigabe aus der Basisdatei. Ohne dieses Tag # !reset loescht die Portfreigabe aus der Basisdatei. Ohne dieses Tag
# wuerden die Listen zusammengefuegt statt ersetzt – Compose behandelt # wuerden die Listen zusammengefuegt statt ersetzt – Compose behandelt
# "ports" beim Zusammenfuehren als anzuhaengende Liste. # "ports" beim Zusammenfuehren als anzuhaengende Liste. Braucht
ports: !reset [] # Compose 2.24 oder neuer.
app:
ports: !reset [] ports: !reset []
networks: networks:
# Das stackeigene Netz: darueber spricht die Anwendung mit der Datenbank. # Das stackeigene Netz: darueber spricht die Anwendung mit der Datenbank.
+20 -12
View File
@@ -1,14 +1,20 @@
name: nebula 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: # 1) Alles im Container (Server wie Entwicklung):
# docker compose up -d db # 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 # npm run dev
# #
# 2) Alles im Container: # 3) Server ohne jede Portfreigabe, hinter einem Reverse Proxy im
# docker compose --profile full up -d --build # gemeinsamen Docker-Netz: siehe docker-compose.server.yml
# #
# Optionales DB-Webinterface: # Optionales DB-Webinterface:
# docker compose --profile tools up -d adminer → http://localhost:8080 # docker compose --profile tools up -d adminer → http://localhost:8080
@@ -25,12 +31,10 @@ services:
POSTGRES_DB: ${POSTGRES_DB:-nebula} POSTGRES_DB: ${POSTGRES_DB:-nebula}
# Deterministische Sortierung unabhaengig vom Host-Locale # Deterministische Sortierung unabhaengig vom Host-Locale
POSTGRES_INITDB_ARGS: "--encoding=UTF8" POSTGRES_INITDB_ARGS: "--encoding=UTF8"
ports: # Absichtlich keine Portfreigabe: die Anwendung erreicht die Datenbank im
# Nur fuer die Entwicklung – die Anwendung erreicht die Datenbank im # Container ueber "db:5432", und backup.sh geht ueber "docker exec". Nach
# Container ueber "db:5432", nicht ueber den Host. Deshalb an 127.0.0.1 # aussen muss hier nichts. Wer die Datenbank vom Host aus braucht – fuer
# gebunden. Im Serverbetrieb faellt die Freigabe ganz weg, siehe # "npm run dev" oder psql – nimmt docker-compose.dev.yml dazu.
# docker-compose.server.yml.
- "${POSTGRES_PORT:-127.0.0.1:5432}:5432"
volumes: volumes:
- nebula-db-data:/var/lib/postgresql/data - nebula-db-data:/var/lib/postgresql/data
healthcheck: healthcheck:
@@ -77,7 +81,11 @@ services:
PORT: 3000 PORT: 3000
HOSTNAME: 0.0.0.0 HOSTNAME: 0.0.0.0
ports: 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: adminer:
profiles: ["tools"] profiles: ["tools"]
+1 -1
View File
@@ -16,7 +16,7 @@
"db:studio": "prisma studio", "db:studio": "prisma studio",
"db:seed": "prisma db seed", "db:seed": "prisma db seed",
"db:reset": "prisma migrate reset", "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:db:down": "docker compose stop db",
"docker:up": "docker compose --profile full up -d --build", "docker:up": "docker compose --profile full up -d --build",
"docker:down": "docker compose --profile full down", "docker:down": "docker compose --profile full down",