forked from jappel/leistungsbilanz-ts
Run the Docker stack on built output instead of dev servers
compose.yaml overrode the image's own entrypoint to start "tsx watch" and "next dev" with CHOKIDAR_USEPOLLING and WATCHPACK_POLLING enabled. The built dist/ and .next from the image were never used, and the polling watchers kept both containers busy around the clock whether or not anyone used the application. Split the two use cases: compose.yaml now runs node dist/server/index.js and next start with no source mounts and no watchers, and the watching setup moves to compose.dev.yaml. next build writes the rewrite destinations from next.config.mjs into .next/routes-manifest.json, so next start cannot pick up API_INTERNAL_URL at runtime the way next dev does. Pass it as a build argument. Give the web container its own probe. The healthcheck hit "/", which redirects to "/projects", and fetch follows redirects, so every probe rendered two pages every five seconds. "/health" could not serve as the probe because next.config.mjs rewrites it to the API. Add "/web-health" as a route handler, exclude it from the proxy matcher, and relax both intervals to 30s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c6cdfc42d5
commit
906aa751c7
8 changed files with 184 additions and 54 deletions
|
|
@ -5,6 +5,13 @@ COPY package*.json ./
|
|||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
# next build writes the rewrite destinations from next.config.mjs into
|
||||
# .next/routes-manifest.json, so "next start" cannot pick up a different
|
||||
# API URL later. The value has to be known here, not just at runtime.
|
||||
ARG API_INTERNAL_URL=http://localhost:3000
|
||||
ENV API_INTERNAL_URL=$API_INTERNAL_URL
|
||||
|
||||
RUN npm run build:api && npm run build:web
|
||||
|
||||
RUN mkdir -p data && chmod +x scripts/docker-start.sh
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -62,10 +62,24 @@ docker compose logs --follow
|
|||
docker compose down
|
||||
```
|
||||
|
||||
Der Compose-Stack startet Entwicklungsserver mit Quellcode-Mounts. Er ist kein
|
||||
Produktionsdeployment. Details stehen in
|
||||
`compose.yaml` startet den Produktionsstand: gebautes `dist/` und `next start`,
|
||||
ohne Quellcode-Mounts und ohne Datei-Watcher. Details stehen in
|
||||
[Deployment und Betrieb](docs/deployment.md).
|
||||
|
||||
Für die Entwicklung mit Hot Reload gibt es einen eigenen Stack mit
|
||||
Quellcode-Mounts und Watchern:
|
||||
|
||||
```powershell
|
||||
docker compose -f compose.dev.yaml up --build --detach
|
||||
docker compose -f compose.dev.yaml logs --follow
|
||||
docker compose -f compose.dev.yaml down
|
||||
```
|
||||
|
||||
Die Watcher darin laufen im Polling-Modus, weil Bind-Mounts unter Windows und
|
||||
macOS keine inotify-Events durchreichen. Das kostet dauerhaft CPU, auch wenn
|
||||
niemand die Anwendung benutzt — deshalb gehört dieser Stack nicht auf einen
|
||||
Server.
|
||||
|
||||
## Direkte lokale Entwicklung
|
||||
|
||||
Voraussetzungen:
|
||||
|
|
|
|||
87
compose.dev.yaml
Normal file
87
compose.dev.yaml
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Development stack: source mounts, watching dev servers, hot reload.
|
||||
# docker compose -f compose.dev.yaml up --build
|
||||
#
|
||||
# The polling watchers below are needed for bind mounts on Windows and
|
||||
# macOS, where inotify events do not cross the VM boundary. They cost
|
||||
# continuous CPU, which is why the production stack in compose.yaml does
|
||||
# not run watchers at all.
|
||||
name: leistungsbilanz-dev
|
||||
|
||||
x-logging: &logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
max-file: "10"
|
||||
|
||||
services:
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- npm run db:migrate && npm run db:verify:circuit-schema && npm run dev:api
|
||||
environment:
|
||||
PORT: "3000"
|
||||
CHOKIDAR_USEPOLLING: "true"
|
||||
LOG_LEVEL: "${LOG_LEVEL:-debug}"
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
logging: *logging
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./src:/app/src
|
||||
- ./scripts:/app/scripts
|
||||
- ./data:/app/data
|
||||
- ./drizzle.config.ts:/app/drizzle.config.ts:ro
|
||||
- ./tsconfig.json:/app/tsconfig.json:ro
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- fetch('http://localhost:3000/health').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1))
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
command:
|
||||
- npm
|
||||
- run
|
||||
- dev:web
|
||||
- --
|
||||
- --hostname
|
||||
- 0.0.0.0
|
||||
environment:
|
||||
API_INTERNAL_URL: http://api:3000
|
||||
WATCHPACK_POLLING: "true"
|
||||
NEXT_TELEMETRY_DISABLED: "1"
|
||||
LOG_LEVEL: "${LOG_LEVEL:-debug}"
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
logging: *logging
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3001:3001"
|
||||
volumes:
|
||||
- ./src:/app/src
|
||||
- ./next.config.mjs:/app/next.config.mjs:ro
|
||||
- ./tsconfig.json:/app/tsconfig.json:ro
|
||||
- ./tsconfig.next.json:/app/tsconfig.next.json:ro
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- fetch('http://localhost:3001/web-health').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1))
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
65
compose.yaml
65
compose.yaml
|
|
@ -1,82 +1,73 @@
|
|||
name: leistungsbilanz
|
||||
|
||||
x-build: &build
|
||||
context: .
|
||||
args:
|
||||
# Baked into .next/routes-manifest.json by next build; see Dockerfile.
|
||||
API_INTERNAL_URL: http://api:3000
|
||||
|
||||
x-logging: &logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
max-file: "10"
|
||||
|
||||
services:
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
build: *build
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- npm run db:migrate && npm run db:verify:circuit-schema && npm run dev:api
|
||||
- node scripts/run-migrations.js && node scripts/db-verify-circuit-schema.js && node dist/server/index.js
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: "3000"
|
||||
CHOKIDAR_USEPOLLING: "true"
|
||||
LOG_LEVEL: "${LOG_LEVEL:-info}"
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
max-file: "10"
|
||||
logging: *logging
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./src:/app/src
|
||||
- ./scripts:/app/scripts
|
||||
- ./data:/app/data
|
||||
- ./drizzle.config.ts:/app/drizzle.config.ts:ro
|
||||
- ./tsconfig.json:/app/tsconfig.json:ro
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- fetch('http://localhost:3000/health').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1))
|
||||
interval: 5s
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 12
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
build: *build
|
||||
command:
|
||||
- npm
|
||||
- run
|
||||
- dev:web
|
||||
- --
|
||||
- --hostname
|
||||
- 0.0.0.0
|
||||
- node_modules/.bin/next
|
||||
- start
|
||||
- -p
|
||||
- "3001"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
API_INTERNAL_URL: http://api:3000
|
||||
WATCHPACK_POLLING: "true"
|
||||
NEXT_TELEMETRY_DISABLED: "1"
|
||||
LOG_LEVEL: "${LOG_LEVEL:-info}"
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
max-file: "10"
|
||||
logging: *logging
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3001:3001"
|
||||
volumes:
|
||||
- ./src:/app/src
|
||||
- ./next.config.mjs:/app/next.config.mjs:ro
|
||||
- ./tsconfig.json:/app/tsconfig.json:ro
|
||||
- ./tsconfig.next.json:/app/tsconfig.next.json:ro
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- fetch('http://localhost:3001/').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1))
|
||||
interval: 5s
|
||||
- fetch('http://localhost:3001/web-health').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1))
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 12
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
|
|
|||
|
|
@ -2,19 +2,27 @@
|
|||
|
||||
## Aktueller Status
|
||||
|
||||
Es gibt derzeit kein unterstütztes Produktionsdeployment.
|
||||
Es gibt zwei Compose-Stacks.
|
||||
|
||||
`compose.yaml` ist ausschließlich für lokale Entwicklung vorgesehen. Es startet
|
||||
`tsx watch` und `next dev`, bindet Quellcode vom Host ein und enthält weder TLS,
|
||||
Authentifizierung, Reverse Proxy, Prozesshärtung noch ein zentral betriebenes
|
||||
Datenbanksystem. Der Stack darf deshalb nicht als produktionsreif bezeichnet oder
|
||||
öffentlich erreichbar gemacht werden.
|
||||
`compose.yaml` startet den gebauten Stand: `node dist/server/index.js` und
|
||||
`next start`, ohne Quellcode-Mounts und ohne Datei-Watcher. Das ist der Stack
|
||||
für einen Server.
|
||||
|
||||
## Entwicklungs-Topologie
|
||||
`compose.dev.yaml` startet `tsx watch` und `next dev` und bindet Quellcode vom
|
||||
Host ein. Die Watcher laufen im Polling-Modus, weil Bind-Mounts unter Windows
|
||||
und macOS keine inotify-Events durchreichen; das kostet dauerhaft CPU, auch
|
||||
ohne Benutzeraktivität. Dieser Stack gehört deshalb nur auf einen
|
||||
Entwicklungsrechner.
|
||||
|
||||
Beides enthält weder TLS, Authentifizierung, Reverse Proxy, Prozesshärtung noch
|
||||
ein zentral betriebenes Datenbanksystem. Der Stack darf deshalb nicht öffentlich
|
||||
erreichbar gemacht werden.
|
||||
|
||||
## Topologie
|
||||
|
||||
| Komponente | Port | Healthcheck | Persistenz |
|
||||
| --- | ---: | --- | --- |
|
||||
| Next.js Web | 3001 | `GET /` | keine |
|
||||
| Next.js Web | 3001 | `GET /web-health` | keine |
|
||||
| Express API | 3000 | `GET /health` | `./data:/app/data` |
|
||||
| SQLite | Datei | Integritäts-/FK-Prüfung via Backup und Skript | `data/leistungsbilanz.db` |
|
||||
|
||||
|
|
@ -24,15 +32,22 @@ Verwendete Umgebungsvariablen:
|
|||
- `API_INTERNAL_URL` – internes API-Ziel des Next.js-Rewrites, im Compose-Netz
|
||||
`http://api:3000`
|
||||
- `NEXT_TELEMETRY_DISABLED=1`
|
||||
- `CHOKIDAR_USEPOLLING=true` und `WATCHPACK_POLLING=true` für lokale
|
||||
Dateibeobachtung in Docker
|
||||
- `CHOKIDAR_USEPOLLING=true` und `WATCHPACK_POLLING=true` – nur in
|
||||
`compose.dev.yaml`, für Dateibeobachtung über Bind-Mounts hinweg
|
||||
- `LOG_LEVEL` – steuert für beide Dienste die Ausgabestufe des strukturierten
|
||||
JSON-Loggers (`error`, `warn`, `info`, `verbose`, `debug`), Standard `info`.
|
||||
Setzbar über eine `.env`-Datei neben `compose.yaml` oder
|
||||
`LOG_LEVEL=verbose docker compose up`.
|
||||
|
||||
Beim API-Start laufen zuerst `npm run db:migrate` und
|
||||
`npm run db:verify:circuit-schema`.
|
||||
Beim API-Start laufen zuerst die Migrationen und die Schemaprüfung
|
||||
(`scripts/run-migrations.js` und `scripts/db-verify-circuit-schema.js`, im
|
||||
Entwicklungsstack über `npm run db:migrate` und
|
||||
`npm run db:verify:circuit-schema`).
|
||||
|
||||
`API_INTERNAL_URL` wirkt für `next start` zur **Build-Zeit**: `next build`
|
||||
schreibt die Rewrite-Ziele aus `next.config.mjs` fest in
|
||||
`.next/routes-manifest.json`. `compose.yaml` reicht den Wert deshalb als
|
||||
Build-Argument an das Image durch, nicht nur als Laufzeit-Variable.
|
||||
|
||||
## Logging
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
"docker:up": "docker compose up --build --detach",
|
||||
"docker:down": "docker compose down",
|
||||
"docker:logs": "docker compose logs --follow",
|
||||
"docker:dev:up": "docker compose -f compose.dev.yaml up --build --detach",
|
||||
"docker:dev:down": "docker compose -f compose.dev.yaml down",
|
||||
"docker:dev:logs": "docker compose -f compose.dev.yaml logs --follow",
|
||||
"build": "npm run build:api",
|
||||
"build:api": "tsc -p tsconfig.json",
|
||||
"build:web": "next build",
|
||||
|
|
|
|||
10
src/app/web-health/route.ts
Normal file
10
src/app/web-health/route.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
// Liveness probe for the web container itself. The "/health" path is
|
||||
// rewritten to the API in next.config.mjs, so it cannot answer for this
|
||||
// process. Kept as a route handler so a probe does not render a page.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export function GET() {
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
|
@ -5,8 +5,9 @@ import { createLogger } from "./shared/logging/logger";
|
|||
const logger = createLogger("web:navigation");
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
// The Docker healthcheck hits "/" every few seconds with no User-Agent
|
||||
// header; skip it so real navigation isn't drowned out in the logs.
|
||||
// Probes hit "/web-health" and are excluded by the matcher below. The
|
||||
// User-Agent guard stays as a fallback for anything else that polls
|
||||
// without one, so real navigation isn't drowned out in the logs.
|
||||
if (request.headers.get("user-agent")) {
|
||||
logger.info("page request", {
|
||||
method: request.method,
|
||||
|
|
@ -17,5 +18,7 @@ export function proxy(request: NextRequest) {
|
|||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/((?!_next/static|_next/image|favicon.ico|api).*)"],
|
||||
matcher: [
|
||||
"/((?!_next/static|_next/image|favicon.ico|api|web-health).*)",
|
||||
],
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue