fix(docker): keine Ports auf 0.0.0.0 und ein Overlay ohne Freigaben
CI / pruefen (push) Has been cancelled

Der Datenbankport wurde in beiden Betriebsarten auf 0.0.0.0 gelegt, obwohl
ihn nur die Entwicklung braucht: im Container spricht die Anwendung "db:5432"
ueber das Compose-Netz an, und backup.sh geht ueber docker exec. Auf einem
Server lauschte Postgres damit im Netz, geschuetzt allein durch das Passwort
aus der .env. Die Anleitung riet zwar zu 127.0.0.1, aber als Handgriff statt
als Voreinstellung -- die falsche Richtung. Datenbank und Adminer binden
jetzt von Haus aus auf 127.0.0.1.

Liegen mehrere Stacks auf einem Host und regelt ein gemeinsamer Reverse Proxy
den Zugang, braucht Nebula gar keinen Host-Port. docker-compose.server.yml
nimmt beide Freigaben zurueck und haengt die Anwendung in ein geteiltes Netz.
Das braucht !reset: Compose fuehrt "ports" beim Zusammenfuehren anhaengend
statt ersetzend zusammen, eine leere Liste im Override wuerde also nichts
loeschen.

update.sh las die Adresse aus APP_PORT und haette in diesem Modus einen Port
gemeldet, den es nicht gibt. Es fragt jetzt Docker und sagt sonst, dass nur
das interne Netz erreichbar ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 09:19:51 +02:00
co-authored by Claude Opus 5
parent 6808d4d34a
commit d311fe48c0
6 changed files with 152 additions and 26 deletions
+16 -3
View File
@@ -38,10 +38,23 @@ 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 # Host-Port, unter dem die Datenbank erreichbar ist. Nur die Entwicklung
POSTGRES_PORT="5432" # braucht ihn: im Container spricht die Anwendung "db:5432" an. Deshalb an
# 127.0.0.1 gebunden, damit er nicht im Netz steht.
POSTGRES_PORT="127.0.0.1:5432"
# Host-Port fuer Adminer (--profile tools) # Host-Port fuer Adminer (--profile tools)
ADMINER_PORT="8080" ADMINER_PORT="127.0.0.1:8080"
# ── Betrieb ohne veroeffentlichte Ports ──────────────────────
# Liegen mehrere Stacks auf demselben Docker-Host und regelt ein gemeinsamer
# Reverse Proxy den Zugang, veroeffentlicht Nebula gar keinen Port. Dann diese
# beiden Zeilen aktivieren – danach genuegt ueberall "docker compose up -d":
# COMPOSE_FILE="docker-compose.yml:docker-compose.server.yml"
# COMPOSE_PROFILES="full"
#
# Name des geteilten Netzes und der Alias, unter dem der Proxy Nebula findet.
# PROXY_NETWORK="proxy"
# PROXY_ALIAS="nebula"
# ── Sicherheit ─────────────────────────────────────────────── # ── Sicherheit ───────────────────────────────────────────────
# Schluessel zur Verschluesselung der Git-Access-Tokens (AES-256-GCM). # Schluessel zur Verschluesselung der Git-Access-Tokens (AES-256-GCM).
+68 -10
View File
@@ -86,19 +86,54 @@ APP_PORT=127.0.0.1:3000
> **`APP_URL` später zu ändern** macht bestehende **Passkeys ungültig**, weil > **`APP_URL` später zu ändern** macht bestehende **Passkeys ungültig**, weil
> sie an die rpID gebunden sind. Passwörter funktionieren weiter. > sie an die rpID gebunden sind. Passwörter funktionieren weiter.
### Datenbankport schließen ### Welche Ports überhaupt hinausgehen
`docker-compose.yml` bildet Postgres standardmäßig auf `5432` ab, damit man | Container | Host-Port | Wann | Wird gebraucht? |
in der Entwicklung von außen drankommt. Auf einem Server hat die Datenbank | --- | --- | --- | --- |
nichts im Internet zu suchen: | `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 |
```env **Der Datenbankport muss nicht nach außen.** Im Vollbetrieb spricht die
POSTGRES_PORT=127.0.0.1:5432 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.
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.
### 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.
```bash
docker network create proxy # falls der Proxy-Stack es nicht schon anlegt
``` ```
Noch besser: die `ports:`-Zeile des `db`-Dienstes ganz entfernen. Die ```env
Anwendung erreicht die Datenbank über das interne Docker-Netz, nicht über # .env
den Host. COMPOSE_FILE="docker-compose.yml:docker-compose.server.yml"
COMPOSE_PROFILES="full"
PROXY_NETWORK="proxy" # so heißt das Netz
PROXY_ALIAS="nebula" # so findet der Proxy die Anwendung
```
Compose liest beides aus der `.env`. Danach genügt überall — auch in
`scripts/update.sh` — ein schlichtes `docker compose up -d`. Im Proxy zeigt
das Ziel dann nicht mehr auf `127.0.0.1:3000`, sondern auf **`nebula:3000`**;
Schritt 6 zeigt beide Schreibweisen.
> Ohne die Datei würde ein Override die Portlisten **zusammenhängen** statt
> sie zu ersetzen — Compose führt `ports` anfügend zusammen. Das `!reset`
> darin ist genau dafür da und braucht Compose 2.24 oder neuer.
>
> Das Trennzeichen in `COMPOSE_FILE` ist unter Linux der Doppelpunkt, unter
> Windows das Semikolon.
## 4 · Starten ## 4 · Starten
@@ -114,6 +149,13 @@ docker compose --profile full logs -f app # mitlesen, Strg+C beendet nur das
curl -s localhost:3000/api/health # {"status":"ok","database":"up",…} curl -s localhost:3000/api/health # {"status":"ok","database":"up",…}
``` ```
Ohne veröffentlichten Port geht die Frage an den Container statt an den Host
— das Abbild bringt einen `HEALTHCHECK` mit:
```bash
docker inspect --format '{{.State.Health.Status}}' nebula-app # healthy
```
## 5 · Ersten Zugang anlegen ## 5 · Ersten Zugang anlegen
Ruf `https://nebula.deine-domain.de/setup` auf (nach Schritt 6) oder Ruf `https://nebula.deine-domain.de/setup` auf (nach Schritt 6) oder
@@ -151,6 +193,16 @@ sudo systemctl reload caddy
Das genügt. Caddy setzt `X-Forwarded-*` von selbst, holt das Zertifikat bei Das genügt. Caddy setzt `X-Forwarded-*` von selbst, holt das Zertifikat bei
Let's Encrypt und leitet `http` auf `https` um. Let's Encrypt und leitet `http` auf `https` um.
Läuft der Proxy selbst als Container im geteilten Netz (die Variante ohne
veröffentlichte Ports), lautet das Ziel stattdessen `nebula:3000` —
Docker löst den Alias auf:
```caddyfile
nebula.deine-domain.de {
reverse_proxy nebula:3000
}
```
<details> <details>
<summary>Dasselbe mit nginx</summary> <summary>Dasselbe mit nginx</summary>
@@ -189,7 +241,13 @@ sudo ufw enable
``` ```
Port 3000 und 5432 bleiben **zu**. Beide sind nach Schritt 3 ohnehin nur an Port 3000 und 5432 bleiben **zu**. Beide sind nach Schritt 3 ohnehin nur an
`127.0.0.1` gebunden; die Firewall ist der zweite Riegel. `127.0.0.1` gebunden — oder gar nicht erst veröffentlicht; die Firewall ist
der zweite Riegel.
> 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` sie zu melden scheint. Genau deshalb wird hier an `127.0.0.1`
> gebunden, statt sich auf die Firewall zu verlassen.
## 8 · Sicherungen automatisieren ## 8 · Sicherungen automatisieren
+2 -2
View File
@@ -122,8 +122,8 @@ Alle Variablen sind in [`.env.example`](.env.example) dokumentiert.
| `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 | `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 | `5432` | | `POSTGRES_PORT` | Host-Port der Datenbank — nur für die Entwicklung, im Container läuft es über `db:5432` | `127.0.0.1:5432` |
| `ADMINER_PORT` | Host-Port von Adminer (`--profile tools`) | `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` |
| `GIT_BLOCK_PRIVATE` | Zugriff ins private LAN sperren | `false` | | `GIT_BLOCK_PRIVATE` | Zugriff ins private LAN sperren | `false` |
+46
View File
@@ -0,0 +1,46 @@
# ─────────────────────────────────────────────────────────────
# Overlay fuer den Serverbetrieb: kein Port geht nach aussen.
#
# 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.
#
# Aufruf:
# docker compose -f docker-compose.yml -f docker-compose.server.yml \
# --profile full up -d --build
#
# Bequemer ist, das dauerhaft in die .env zu schreiben:
# COMPOSE_FILE=docker-compose.yml:docker-compose.server.yml
# COMPOSE_PROFILES=full
# Danach genuegt ueberall "docker compose up -d" – auch in update.sh.
#
# Das Netz muss es vorher geben (der Proxy-Stack legt es ueblicherweise an):
# docker network create proxy
#
# Im Proxy zeigt das Ziel dann auf http://nebula:3000 bzw. nebula-app:3000.
# ─────────────────────────────────────────────────────────────
services:
db:
# !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: !reset []
networks:
# Das stackeigene Netz: darueber spricht die Anwendung mit der Datenbank.
default:
# Das geteilte Netz zum Reverse Proxy. Der Alias ist der Name, unter dem
# der Proxy Nebula anspricht – kurz und eindeutig, denn "app" hiessen in
# einem geteilten Netz schnell mehrere Container.
proxy:
aliases:
- ${PROXY_ALIAS:-nebula}
networks:
proxy:
external: true
name: ${PROXY_NETWORK:-proxy}
+6 -2
View File
@@ -26,7 +26,11 @@ services:
# Deterministische Sortierung unabhaengig vom Host-Locale # Deterministische Sortierung unabhaengig vom Host-Locale
POSTGRES_INITDB_ARGS: "--encoding=UTF8" POSTGRES_INITDB_ARGS: "--encoding=UTF8"
ports: ports:
- "${POSTGRES_PORT:-5432}:5432" # 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"
volumes: volumes:
- nebula-db-data:/var/lib/postgresql/data - nebula-db-data:/var/lib/postgresql/data
healthcheck: healthcheck:
@@ -87,7 +91,7 @@ services:
ADMINER_DEFAULT_SERVER: db ADMINER_DEFAULT_SERVER: db
ADMINER_DESIGN: dracula ADMINER_DESIGN: dracula
ports: ports:
- "${ADMINER_PORT:-8080}:8080" - "${ADMINER_PORT:-127.0.0.1:8080}:8080"
volumes: volumes:
nebula-db-data: nebula-db-data:
+12 -7
View File
@@ -164,19 +164,24 @@ if [ "$gesund" -ne 1 ]; then
fi fi
ok "Container meldet sich gesund" ok "Container meldet sich gesund"
# Die Anzeige der Adresse: aus APP_PORT kann "3000" oder "127.0.0.1:3000" # Die Anzeige der Adresse. Nicht die .env raten, sondern Docker fragen: mit
# kommen – fuer die Ausgabe zaehlt nur, was hinter dem letzten Doppelpunkt steht. # docker-compose.server.yml veroeffentlicht der Container gar keinen Port und
port_roh="$(sed -n 's/^[[:space:]]*APP_PORT[[:space:]]*=[[:space:]]*"\{0,1\}\([^"#]*\)"\{0,1\}[[:space:]]*$/\1/p' "$root/.env" | tail -n 1)" # haengt nur im Netz des Reverse Proxy. Dann gibt "port" nichts zurueck.
port_roh="${port_roh:-3000}" adresse="$(docker compose --profile full port app 3000 2>/dev/null | tail -n 1 || true)"
port="${port_roh##*:}"
if command -v curl >/dev/null 2>&1; then if [ -n "$adresse" ]; then
port="${adresse##*:}"
if command -v curl >/dev/null 2>&1; then
antwort_json="$(curl -fsS "http://127.0.0.1:${port}/api/health" 2>/dev/null || true)" antwort_json="$(curl -fsS "http://127.0.0.1:${port}/api/health" 2>/dev/null || true)"
[ -n "$antwort_json" ] && ok "Antwort: $antwort_json" [ -n "$antwort_json" ] && ok "Antwort: $antwort_json"
fi
wo="$adresse"
else
wo="im Docker-Netz (kein Host-Port veroeffentlicht)"
fi fi
# ── Fertig ─────────────────────────────────────────────────── # ── Fertig ───────────────────────────────────────────────────
version_jetzt="$(paket_version)" version_jetzt="$(paket_version)"
printf '\n%s%s✓ Nebula v%s laeuft auf %s%s\n' "$gruen" "$fett" "$version_jetzt" "$port_roh" "$aus" printf '\n%s%s✓ Nebula v%s laeuft auf %s%s\n' "$gruen" "$fett" "$version_jetzt" "$wo" "$aus"
[ -n "$sicherung" ] && printf ' Sicherung vor dem Update: %s\n' "$sicherung" [ -n "$sicherung" ] && printf ' Sicherung vor dem Update: %s\n' "$sicherung"
printf ' Log mitlesen: docker compose --profile full logs -f app\n\n' printf ' Log mitlesen: docker compose --profile full logs -f app\n\n'