diff --git a/.env.example b/.env.example index b4bc398..685dbc6 100644 --- a/.env.example +++ b/.env.example @@ -38,10 +38,23 @@ 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 -POSTGRES_PORT="5432" +# 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. +POSTGRES_PORT="127.0.0.1:5432" # 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 ─────────────────────────────────────────────── # Schluessel zur Verschluesselung der Git-Access-Tokens (AES-256-GCM). diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 1d66048..ef8b05d 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -86,19 +86,54 @@ APP_PORT=127.0.0.1:3000 > **`APP_URL` später zu ändern** macht bestehende **Passkeys ungültig**, weil > 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 -in der Entwicklung von außen drankommt. Auf einem Server hat die Datenbank -nichts im Internet zu suchen: +| 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 | -```env -POSTGRES_PORT=127.0.0.1:5432 +**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. + +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 -Anwendung erreicht die Datenbank über das interne Docker-Netz, nicht über -den Host. +```env +# .env +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 @@ -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",…} ``` +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 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 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 +} +``` +
Dasselbe mit nginx @@ -189,7 +241,13 @@ sudo ufw enable ``` 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 diff --git a/README.md b/README.md index 6090ef7..7eec54c 100644 --- a/README.md +++ b/README.md @@ -122,8 +122,8 @@ Alle Variablen sind in [`.env.example`](.env.example) dokumentiert. | `PORT` | Port der Anwendung | `3000` | | `APP_PORT` | Host-Port-Mapping des App-Containers | `3000` | | `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` | Zugangsdaten des DB-Containers | `nebula` | -| `POSTGRES_PORT` | Host-Port der Datenbank | `5432` | -| `ADMINER_PORT` | Host-Port von Adminer (`--profile tools`) | `8080` | +| `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`) | `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` | | `GIT_BLOCK_PRIVATE` | Zugriff ins private LAN sperren | `false` | diff --git a/docker-compose.server.yml b/docker-compose.server.yml new file mode 100644 index 0000000..6b86131 --- /dev/null +++ b/docker-compose.server.yml @@ -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} diff --git a/docker-compose.yml b/docker-compose.yml index 622ea16..3642616 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,11 @@ services: # Deterministische Sortierung unabhaengig vom Host-Locale POSTGRES_INITDB_ARGS: "--encoding=UTF8" 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: - nebula-db-data:/var/lib/postgresql/data healthcheck: @@ -87,7 +91,7 @@ services: ADMINER_DEFAULT_SERVER: db ADMINER_DESIGN: dracula ports: - - "${ADMINER_PORT:-8080}:8080" + - "${ADMINER_PORT:-127.0.0.1:8080}:8080" volumes: nebula-db-data: diff --git a/scripts/update.sh b/scripts/update.sh index ce3f405..3c2cbfe 100644 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -164,19 +164,24 @@ if [ "$gesund" -ne 1 ]; then fi ok "Container meldet sich gesund" -# Die Anzeige der Adresse: aus APP_PORT kann "3000" oder "127.0.0.1:3000" -# kommen – fuer die Ausgabe zaehlt nur, was hinter dem letzten Doppelpunkt steht. -port_roh="$(sed -n 's/^[[:space:]]*APP_PORT[[:space:]]*=[[:space:]]*"\{0,1\}\([^"#]*\)"\{0,1\}[[:space:]]*$/\1/p' "$root/.env" | tail -n 1)" -port_roh="${port_roh:-3000}" -port="${port_roh##*:}" +# Die Anzeige der Adresse. Nicht die .env raten, sondern Docker fragen: mit +# docker-compose.server.yml veroeffentlicht der Container gar keinen Port und +# haengt nur im Netz des Reverse Proxy. Dann gibt "port" nichts zurueck. +adresse="$(docker compose --profile full port app 3000 2>/dev/null | tail -n 1 || true)" -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)" - [ -n "$antwort_json" ] && ok "Antwort: $antwort_json" +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)" + [ -n "$antwort_json" ] && ok "Antwort: $antwort_json" + fi + wo="$adresse" +else + wo="im Docker-Netz (kein Host-Port veroeffentlicht)" fi # ── Fertig ─────────────────────────────────────────────────── 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" printf ' Log mitlesen: docker compose --profile full logs -f app\n\n'