From 6afaa04a957545e62453b73e94bf9dd4b3685c12 Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 21 May 2026 02:46:46 +0300 Subject: [PATCH] fix(docker): supervise self-hosted runtime processes --- docker/README.md | 26 +++++++++--------- docker/docker-compose.yml | 1 + docker/docker-entrypoint.sh | 40 +++++++++++++++++++++++++--- docs/architecture/pwa-self-hosted.md | 4 ++- 4 files changed, 55 insertions(+), 16 deletions(-) diff --git a/docker/README.md b/docker/README.md index c34798edc..bc585c047 100644 --- a/docker/README.md +++ b/docker/README.md @@ -43,12 +43,12 @@ before it creates `PwaService`. These variables are supported by the Docker image. The compose file sets the safe local defaults shown below. -| Variable | Default | Purpose | -| ---------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `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. Multiple origins can be comma-separated. | -| `PORT` | `3000` | Internal Express backend port. nginx proxy config is rendered from the template to match it at startup. | -| `IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS` | `0` | Set to `1` or `true` only for trusted local/LAN deployments that intentionally proxy private network IPTV or mock endpoints. | +| Variable | Default | Purpose | +| ---------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `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. Multiple origins can be comma-separated. | +| `PORT` | `3000` | Internal Express backend port. nginx proxy config is rendered from the template to match it at startup. | +| `IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS` | `0` | Set to `1` or `true` only for trusted local/LAN deployments that intentionally proxy private network IPTV or mock endpoints. | | `NODE_EXTRA_CA_CERTS` | unset | Optional Node.js CA bundle path for providers using private certificate authorities. Mount the CA file into the container and set this path. | The web backend proxy accepts only `http` and `https` provider URLs. The PWA @@ -64,18 +64,20 @@ enabled and pass the CA bundle to Node: ```yaml services: - iptvnator: - volumes: - - ./ca.pem:/etc/ssl/private/provider-ca.pem:ro - environment: - NODE_EXTRA_CA_CERTS: /etc/ssl/private/provider-ca.pem + iptvnator: + volumes: + - ./ca.pem:/etc/ssl/private/provider-ca.pem:ro + environment: + NODE_EXTRA_CA_CERTS: /etc/ssl/private/provider-ca.pem ``` 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`. +define a health check against `/api/health`. If nginx or the backend exits +after startup, the entrypoint exits the container so Docker Compose can apply +the `restart: unless-stopped` policy. ## Local Validation diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c2f17cb2d..4e03f53bc 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -5,6 +5,7 @@ services: build: context: .. dockerfile: docker/Dockerfile + restart: unless-stopped ports: - "4333:80" environment: diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 1ed855009..c04f1a7d9 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -33,13 +33,24 @@ cleanup() { if [ -n "${NGINX_PID:-}" ]; then kill "$NGINX_PID" 2>/dev/null || true + wait "$NGINX_PID" 2>/dev/null || true fi if [ -n "${BACKEND_PID:-}" ]; then kill "$BACKEND_PID" 2>/dev/null || true + wait "$BACKEND_PID" 2>/dev/null || true fi } +process_is_running() { + if [ ! -r "/proc/$1/stat" ]; then + return 1 + fi + + state="$(awk '{ print $3 }' "/proc/$1/stat" 2>/dev/null || true)" + [ -n "$state" ] && [ "$state" != "Z" ] +} + trap cleanup INT TERM EXIT backend_ready=0 @@ -50,9 +61,12 @@ for _ in $(seq 1 30); do break fi - if ! kill -0 "$BACKEND_PID" 2>/dev/null; then + if ! process_is_running "$BACKEND_PID"; then + set +e wait "$BACKEND_PID" - exit $? + BACKEND_STATUS=$? + set -e + exit "$BACKEND_STATUS" fi sleep 1 @@ -66,4 +80,24 @@ fi nginx -g 'daemon off;' & NGINX_PID=$! -wait "$NGINX_PID" +while :; do + if ! process_is_running "$BACKEND_PID"; then + set +e + wait "$BACKEND_PID" + EXIT_STATUS=$? + set -e + echo "IPTVnator web backend exited with status ${EXIT_STATUS}." + exit "$EXIT_STATUS" + fi + + if ! process_is_running "$NGINX_PID"; then + set +e + wait "$NGINX_PID" + EXIT_STATUS=$? + set -e + echo "nginx exited with status ${EXIT_STATUS}." + exit "$EXIT_STATUS" + fi + + sleep 1 +done diff --git a/docs/architecture/pwa-self-hosted.md b/docs/architecture/pwa-self-hosted.md index 08b488888..6920af21b 100644 --- a/docs/architecture/pwa-self-hosted.md +++ b/docs/architecture/pwa-self-hosted.md @@ -109,7 +109,9 @@ The Docker image has two stages: 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. + backend, waits for `/health`, and then starts nginx. If either process exits + after startup, the entrypoint exits the container so the compose restart + policy can recover the service. Default runtime values: