From fd594470644f3b57fdaba9e5ebd9bad94e47d36d Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Thu, 21 May 2026 09:30:57 +0200 Subject: [PATCH] docs(docker): restore self-hosted PWA compose flow * docs(docker): restore self-hosted PWA compose flow * fix(docker): supervise self-hosted runtime processes * fix(docker): exit cleanly on container stop --- README.md | 18 +++- docker/Dockerfile | 42 +++++--- docker/README.md | 92 ++++++++++++++-- docker/docker-compose.yml | 27 ++--- docker/docker-entrypoint.sh | 109 +++++++++++++++++++ docker/nginx.conf | 35 +++++-- docs/architecture/pwa-self-hosted.md | 150 +++++++++++++++++++++++++++ 7 files changed, 422 insertions(+), 51 deletions(-) create mode 100644 docker/docker-entrypoint.sh create mode 100644 docs/architecture/pwa-self-hosted.md diff --git a/README.md b/README.md index a7d1c594b..85497b279 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ The application is a cross-platform, open-source project built with Electron and - Polish - Custom "User Agent" header configuration for playlists - Light and Dark themes -- Docker version available for self-hosting +- Docker image available for self-hosting the PWA and web backend together ## Screenshots: @@ -79,6 +79,22 @@ The application is a cross-platform, open-source project built with Electron and _Note: First version of the application which was developed as a PWA is available in an extra git branch._ +## Self-hosted PWA + +The Docker setup builds the Angular PWA and the monorepo web backend into one +image. The backend handles remote M3U parsing plus Xtream and Stalker proxy +requests under `/api`, so a separate `4gray/iptvnator-backend` container is not +required for the default self-hosted flow. + +```bash +docker compose -f docker/docker-compose.yml up --build -d +``` + +The application is available at . See +[`docker/docker-compose.yml`](./docker/docker-compose.yml) for the ready-to-run +compose file and [`docker/README.md`](./docker/README.md) for environment +variables, reverse proxy notes, and build details. + ## Download Download the latest version of the application for macOS, Windows, and Linux from the [release page](https://github.com/4gray/iptvnator/releases). diff --git a/docker/Dockerfile b/docker/Dockerfile index 04fe1d685..240597b51 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,32 +1,40 @@ -# Stage 1 - build environment FROM node:22-alpine AS build RUN apk add --no-cache python3 make g++ git -# Create app directory WORKDIR /usr/src/app -# Swtich to node user -#RUN chown node:node ./ -#USER node - COPY .npmrc ./ COPY package.json pnpm-lock.yaml ./ +COPY patches ./patches -# Install app dependencies -RUN corepack enable && pnpm install --frozen-lockfile +RUN corepack enable && pnpm install --frozen-lockfile --ignore-scripts -# Copy all required files COPY . . -# Build the application -RUN pnpm run build:web +RUN pnpm nx build web --configuration=pwa +RUN pnpm nx build web-backend -# Stage 2 - the production environment -FROM nginx:stable-alpine +FROM node:22-alpine -# Copy artifacts and nignx.conf -COPY --from=build /usr/src/app/dist/browser /usr/share/nginx/html -COPY --from=build /usr/src/app/docker/nginx.conf /etc/nginx/conf.d/default.conf +RUN apk add --no-cache gettext nginx -CMD sed -i "s#http://localhost:3333#$BACKEND_URL#g" /usr/share/nginx/html/main.js && nginx -g 'daemon off;' +WORKDIR /opt/iptvnator + +ENV PORT=3000 +ENV BACKEND_URL=/api +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.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 120944a0f..bc585c047 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,20 +1,90 @@ -# Self-hosted version of IPTVnator +# Self-hosted IPTVnator -You can deploy and run the PWA version of IPTVnator on your own machine with `docker-compose` using the following command: +The self-hosted image contains both pieces required for the browser PWA: - $ cd docker - $ docker-compose up -d +- Angular PWA static files served by nginx +- The monorepo `web-backend` Express app proxied under `/api` -This command will launch the frontend and backend applications. By default, the application will be available at: http://localhost:4333/. The ports can be configured in the `docker-compose.yml` file. +The historical standalone `4gray/iptvnator-backend` image is no longer needed +for the default Docker deployment. -The web backend proxy accepts only `http` and `https` provider URLs. The PWA first registers provider URLs through `/provider-targets`, then uses the returned `targetId` for playlist, Xtream, and Stalker proxy calls. The backend blocks loopback, private, link-local, and reserved network targets by default to avoid exposing the self-hosted server as a generic internal-network fetcher. If you intentionally need to test against local mock servers or LAN-only IPTV sources, set `IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS=1` on the backend container and avoid exposing that instance to untrusted users. +## Run With Docker Compose -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`. +From the repository root: -## Build frontend +```bash +docker compose -f docker/docker-compose.yml up --build -d +``` - $ docker build -t 4gray/iptvnator -f docker/Dockerfile . +The ready-to-run compose file is [`docker-compose.yml`](./docker-compose.yml). +By default the app is available at . No additional +environment variables, backend repository checkout, or separate backend +container are required for the default local deployment. -## Build backend +## Build The Image -You can find the backend app with all instructions in a separate GitHub repository - https://github.com/4gray/iptvnator-backend +```bash +docker build -t 4gray/iptvnator -f docker/Dockerfile . +``` + +The image build runs: + +```bash +pnpm nx build web --configuration=pwa +pnpm nx build web-backend +``` + +## Runtime Configuration + +The container writes `/usr/share/nginx/html/assets/app-config.js` on startup. +That file sets `window.__IPTVNATOR_CONFIG__.BACKEND_URL`, which the PWA reads +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. | +| `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 +first registers provider URLs through `/provider-targets`, then uses the +returned `targetId` for playlist, Xtream, and Stalker proxy calls. The backend +blocks loopback, private, link-local, and reserved network targets by default so +a publicly exposed instance cannot be used as a generic internal-network +fetcher. If you enable `IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS=1`, keep the +instance restricted to trusted users. + +For providers that use private certificate authorities, keep TLS validation +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 +``` + +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`. 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 + +```bash +pnpm nx test web-backend +pnpm nx build web --configuration=pwa --skip-nx-cache +pnpm nx build web-backend +pnpm nx run web-e2e:e2e -- --project=chromium --grep @self-hosted +docker compose -f docker/docker-compose.yml config +``` diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d36ab1c08..4e03f53bc 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,18 +1,21 @@ --- services: - backend: - image: 4gray/iptvnator-backend:latest - ports: - - "7333:3000" - environment: - - CLIENT_URL=http://localhost:4333 - # this one should match with the address and port in frontend CLIENT_URL env - - frontend: + iptvnator: image: 4gray/iptvnator:latest + build: + context: .. + dockerfile: docker/Dockerfile + restart: unless-stopped ports: - "4333:80" environment: - - BACKEND_URL=http://localhost:7333 - # this one should match with the address of the backend service - + BACKEND_URL: /api + CLIENT_URL: http://localhost:4333 + PORT: "3000" + IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS: "0" + 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 new file mode 100644 index 000000000..2ca44aeda --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,109 @@ +#!/bin/sh +set -eu + +export PORT="${PORT:-3000}" +export BACKEND_URL="${BACKEND_URL:-/api}" + +envsubst '${PORT}' < /etc/nginx/http.d/default.conf.template > /etc/nginx/http.d/default.conf + +node <<'NODE' +const fs = require('node:fs'); + +fs.writeFileSync( + '/usr/share/nginx/html/assets/app-config.js', + `window.__IPTVNATOR_CONFIG__ = ${JSON.stringify( + { BACKEND_URL: process.env.BACKEND_URL || '/api' }, + null, + 2 + )};\n` +); +NODE + +node /opt/iptvnator/web-backend/main.cjs & +BACKEND_PID=$! + +cleanup_done=0 +cleanup() { + if [ "$cleanup_done" -eq 1 ]; then + return + fi + + cleanup_done=1 + trap - INT TERM EXIT + + 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 +} + +handle_signal() { + cleanup + exit 0 +} + +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 handle_signal INT TERM +trap cleanup EXIT + +backend_ready=0 + +for _ in $(seq 1 30); do + if wget -qO- "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then + backend_ready=1 + break + fi + + if ! process_is_running "$BACKEND_PID"; then + set +e + wait "$BACKEND_PID" + BACKEND_STATUS=$? + set -e + exit "$BACKEND_STATUS" + fi + + sleep 1 +done + +if [ "$backend_ready" -ne 1 ]; then + echo "IPTVnator web backend did not become healthy on port ${PORT}." + exit 1 +fi + +nginx -g 'daemon off;' & +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/docker/nginx.conf b/docker/nginx.conf index 3eacd9674..1abb8aea8 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -1,11 +1,26 @@ server { - listen 80; - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri $uri/ /index.html =404; - } - - include /etc/nginx/extra-conf.d/*.conf; -} \ No newline at end of file + listen 80; + listen [::]:80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://127.0.0.1:${PORT}/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /assets/app-config.js { + add_header Cache-Control "no-store"; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/docs/architecture/pwa-self-hosted.md b/docs/architecture/pwa-self-hosted.md new file mode 100644 index 000000000..6920af21b --- /dev/null +++ b/docs/architecture/pwa-self-hosted.md @@ -0,0 +1,150 @@ +# PWA Self-hosted Architecture + +This document describes the browser PWA and self-hosted Docker path. + +## Ownership + +- `apps/web` owns the Angular browser UI and PWA service worker configuration. +- `apps/web-backend` owns the browser-only backend proxy for remote playlist, + Xtream, and Stalker requests. +- `docker/` owns the production self-hosted image that bundles the PWA and + `web-backend` into one container. + +The old external `4gray/iptvnator-backend` repository is not required for the +default self-hosted deployment. Sync behavior from that repository only when a +change intentionally restores or imports missing backend capabilities. + +## Runtime Backend Configuration + +The PWA reads `window.__IPTVNATOR_CONFIG__.BACKEND_URL` through +`apps/web/src/app/services/runtime-config.ts`. The static placeholder lives at +`apps/web/src/assets/app-config.js` and keeps hosted builds working without +Docker-specific values. + +The Docker entrypoint rewrites `assets/app-config.js` at container startup. +`ngsw-config.json` explicitly excludes this file from Angular service worker +asset hashing so a runtime rewrite does not break cache validation. + +## Service Worker Build + +Use the PWA build configuration for browser deployments: + +```bash +pnpm nx build web --configuration=pwa +``` + +The build must emit these files in `dist/apps/web`: + +- `ngsw-worker.js` +- `ngsw.json` +- `safety-worker.js` +- `worker-basic.min.js` + +`web:serve-static` serves `dist/apps/web` and builds with `web:build:pwa`, so it +exercises the same output layout as Docker. If Nx daemon state returns stale +service worker outputs while changing build options, run: + +```bash +pnpm nx reset +pnpm nx build web --configuration=pwa --skip-nx-cache +``` + +## Web Backend + +The current self-hosted PWA uses these `apps/web-backend` routes: + +- `GET /health` +- `GET /config.js` +- `POST /provider-targets` with `{ "url": "" }` +- `GET /parse?targetId=` +- `GET /xtream?targetId=&username=&password=

&action=` +- `GET /stalker?targetId=&macAddress=&action=` + +The PWA continues to use `PwaService`; only the backend base URL is resolved at +runtime. Electron routes remain owned by the Electron backend and preload +bridge. + +EPG/XMLTV is not a supported self-hosted PWA capability yet. Do not document it +as part of the Docker user flow or use it as the readiness signal for this path. + +Provider URLs are registered before proxy calls so the proxy endpoints do not +accept raw target URLs in query strings. Registration validates the target URL +before any outbound request: + +- only `http:` and `https:` provider URLs are accepted +- URL credentials are rejected +- loopback, private, link-local, and reserved network targets are blocked by + default +- `IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS=1` explicitly enables trusted + local/LAN targets for development, mock servers, or private deployments + +Do not disable TLS certificate validation in the backend proxy. For private +certificate authorities, configure Node with `NODE_EXTRA_CA_CERTS`. + +## PWA Portal User Data + +Xtream favorites and recently viewed items use the browser-side +`PwaXtreamDataSource` when Electron DB preload APIs are unavailable. The PWA +stores this user activity in localStorage: + +- `xtream-favorites` +- `xtream-recent-items` + +Entries should include a content snapshot when the item is added. Global +collection routes and the dashboard can then restore titles, posters, content +type, and category IDs after navigation or a page reload without relying on the +Electron SQLite content table. + +Shared collection services must not import `@iptvnator/portal/xtream/data-access` +directly. Use `XTREAM_COLLECTION_DATA_SOURCE` from +`@iptvnator/portal/shared/util` and bind it to `XTREAM_DATA_SOURCE` at the app +provider boundary (`apps/web/src/app/app.config.ts`). This keeps +`portal-shared-util` provider-neutral and avoids adding new Nx boundary cycles. + +## Docker Runtime + +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. If either process exits + after startup, the entrypoint exits the container so the compose restart + policy can recover the service. + +Default runtime values: + +- `BACKEND_URL=/api` +- `CLIENT_URL=http://localhost:4333` +- `PORT=3000` +- `IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS=0` +- `NODE_EXTRA_CA_CERTS` unset + +When hosting behind another domain, set `CLIENT_URL` to the browser origin and +keep `BACKEND_URL=/api` unless the reverse proxy exposes the backend elsewhere. +Only set `IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS=1` when the self-hosted +instance is restricted to trusted users and intentionally needs private network +IPTV targets. For providers using private certificate authorities, mount the CA +bundle into the container and set `NODE_EXTRA_CA_CERTS` to that mounted path. + +## Validation + +Use the narrow validation ladder for self-hosted changes: + +```bash +pnpm nx test web-backend +pnpm nx test web --runTestsByPath apps/web/src/app/services/runtime-config.spec.ts +pnpm nx build web --configuration=pwa --skip-nx-cache +pnpm nx build web-backend +pnpm nx run web-e2e:e2e -- --project=chromium --grep @self-hosted +docker compose -f docker/docker-compose.yml config +``` + +Run `docker build -t iptvnator:self-hosted-test -f docker/Dockerfile .` when a +Docker daemon is available. + +For manual Docker smoke testing, run the Xtream and Stalker mock servers plus a +small M3U fixture, then verify in the browser that M3U, Xtream, and Stalker can +add sources, play an item, toggle favorites, populate global favorites, +populate recently viewed, and appear on the dashboard rails.