From 8f7d4a5f1800e446dc6ac12d1b85d70d14cac93c Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 16 May 2026 00:23:25 +0200 Subject: [PATCH] fix(docker): wait for self-hosted backend startup --- docker/Dockerfile | 7 +++- docker/README.md | 9 +++-- docker/docker-compose.yml | 6 +++ docker/docker-entrypoint.sh | 58 +++++++++++++++++++++++++++- docker/nginx.conf | 2 +- docs/architecture/pwa-self-hosted.md | 2 + 6 files changed, 77 insertions(+), 7 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index dcb03cc36..e1a21675c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -17,7 +17,7 @@ RUN pnpm nx build web-backend FROM node:22-alpine -RUN apk add --no-cache nginx +RUN apk add --no-cache gettext nginx WORKDIR /opt/iptvnator @@ -27,11 +27,14 @@ ENV CLIENT_URL=http://localhost:4333 COPY --from=build /usr/src/app/dist/apps/web /usr/share/nginx/html COPY --from=build /usr/src/app/dist/apps/web-backend ./web-backend -COPY docker/nginx.conf /etc/nginx/http.d/default.conf +COPY docker/nginx.conf /etc/nginx/http.d/default.conf.template COPY docker/docker-entrypoint.sh /usr/local/bin/iptvnator-entrypoint RUN chmod +x /usr/local/bin/iptvnator-entrypoint EXPOSE 80 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://127.0.0.1/api/health >/dev/null || exit 1 + CMD ["iptvnator-entrypoint"] diff --git a/docker/README.md b/docker/README.md index 399616504..5e08bc108 100644 --- a/docker/README.md +++ b/docker/README.md @@ -39,7 +39,7 @@ before it creates `PwaService`. | ---------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------ | | `BACKEND_URL` | `/api` | Browser-facing backend URL used by the PWA. Keep `/api` for the bundled nginx proxy. | | `CLIENT_URL` | `http://localhost:4333` | Allowed browser origin for backend CORS. Use the public URL when hosting behind a reverse proxy. | -| `PORT` | `3000` | Internal Express backend port. nginx proxy config is patched to match it at container startup. | +| `PORT` | `3000` | Internal Express backend port. nginx proxy config is rendered from the template to match it at startup. | | `IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS` | unset | Set to `1` only for trusted local/LAN deployments that intentionally proxy private network IPTV or mock endpoints. | The web backend proxy accepts only `http` and `https` provider URLs. The PWA @@ -53,8 +53,11 @@ instance restricted to trusted users. For providers that use private certificate authorities, keep TLS validation enabled and pass the CA bundle to Node with `NODE_EXTRA_CA_CERTS=/path/to/ca.pem`. -The nginx config serves the PWA with SPA fallback, avoids caching -`assets/app-config.js`, and proxies `/api/*` to the internal backend. +The entrypoint renders the nginx config from `docker/nginx.conf`, starts the +backend, waits for `/health`, and only then starts nginx. The nginx config +serves the PWA with SPA fallback, avoids caching `assets/app-config.js`, and +proxies `/api/*` to the internal backend. The Dockerfile and compose file both +define a health check against `/api/health`. ## Local Validation diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 11639797b..6e1481937 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -10,3 +10,9 @@ services: environment: BACKEND_URL: /api CLIENT_URL: http://localhost:4333 + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/api/health >/dev/null || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 77466bca1..938fbe887 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -7,7 +7,15 @@ set -eu export PORT BACKEND_URL CLIENT_URL -sed -i "s#127.0.0.1:3000#127.0.0.1:${PORT}#g" /etc/nginx/http.d/default.conf +nginx_template="/etc/nginx/http.d/default.conf.template" +nginx_config="/etc/nginx/http.d/default.conf" + +if ! grep -q '\${PORT}' "$nginx_template"; then + echo "nginx template is missing the \${PORT} backend placeholder" >&2 + exit 1 +fi + +envsubst '${PORT}' < "$nginx_template" > "$nginx_config" node <<'NODE' const fs = require('node:fs'); @@ -31,4 +39,52 @@ shutdown() { trap shutdown INT TERM EXIT +node <<'NODE' +const http = require('node:http'); + +const port = Number.parseInt(process.env.PORT || '3000', 10); +const deadline = Date.now() + 30_000; +const retryDelayMs = 250; + +function retry(error) { + if (Date.now() >= deadline) { + const reason = error?.message ? `: ${error.message}` : ''; + console.error(`web-backend did not become ready on 127.0.0.1:${port}${reason}`); + process.exit(1); + } + + setTimeout(check, retryDelayMs); +} + +function check() { + const request = http.get( + { + host: '127.0.0.1', + path: '/health', + port, + timeout: 1000, + }, + (response) => { + response.resume(); + if ( + response.statusCode && + response.statusCode >= 200 && + response.statusCode < 500 + ) { + process.exit(0); + } + + retry(new Error(`HTTP ${response.statusCode}`)); + } + ); + + request.on('timeout', () => { + request.destroy(new Error('timeout')); + }); + request.on('error', retry); +} + +check(); +NODE + nginx -g 'daemon off;' diff --git a/docker/nginx.conf b/docker/nginx.conf index 5ed772655..9efd47517 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -24,7 +24,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_pass http://127.0.0.1:3000/; + proxy_pass http://127.0.0.1:${PORT}/; } location / { diff --git a/docs/architecture/pwa-self-hosted.md b/docs/architecture/pwa-self-hosted.md index 94ca1cc01..b49f71e18 100644 --- a/docs/architecture/pwa-self-hosted.md +++ b/docs/architecture/pwa-self-hosted.md @@ -106,6 +106,8 @@ The Docker image has two stages: 1. Build stage installs dependencies and runs `web:pwa` plus `web-backend`. 2. Runtime stage uses `node:22-alpine` with nginx installed. nginx serves `dist/apps/web` and proxies `/api/*` to the local Express backend. + The entrypoint renders the nginx config from a `${PORT}` template, starts the + backend, waits for `/health`, and then starts nginx. Default runtime values: