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
This commit is contained in:
4gray authored and GitHub committed 2026-05-21 09:30:57 +02:00
1 parent 738397c9ed
commit fd59447064
7 files changed
+416 -45

No files matched your search

+17 -1
View File
@@ -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 <http://localhost:4333>. 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).
+25 -17
View File
@@ -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"]
+81 -11
View File
@@ -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 <http://localhost:4333>. 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
```
+15 -12
View File
@@ -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
+109
View File
@@ -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
+19 -4
View File
@@ -1,11 +1,26 @@
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html =404;
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;
}
include /etc/nginx/extra-conf.d/*.conf;
location /assets/app-config.js {
add_header Cache-Control "no-store";
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
}
+150
View File
@@ -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": "<provider-url>" }`
- `GET /parse?targetId=<id>`
- `GET /xtream?targetId=<id>&username=<u>&password=<p>&action=<action>`
- `GET /stalker?targetId=<id>&macAddress=<mac>&action=<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.