# NGINX HLS VOD Docker Compose deployment for local MP4 to HLS VOD packaging using Kaltura `nginx-vod-module`. The service maps a media tree read-only into the container and preserves its hierarchy in HLS URLs. ## Scope - ARM64 Linux Docker host - Trusted LAN HTTP service on port `8124` - One H.264 video track and one AAC audio track per MP4 - One configured browser origin for CORS - No authentication, TLS, ABR, external subtitles, or multiple audio/video tracks An attached-picture video stream such as MP4 cover art is ignored by the media validator. ## Configuration Copy the environment template and replace the example player origin: ```sh cp .env.example .env ``` Required values: ```dotenv PLAYER_ORIGIN=http://192.168.1.20:3000 MEDIA_DIR=/srv/media CATALOG_CACHE_DIR=/srv/nginx-hls/catalog-cache HLS_PORT=8124 ``` `MEDIA_DIR=./data` is appropriate only for local development. Production uses an absolute path on the Docker host. The container user must be able to read the mounted files and directories. `CATALOG_CACHE_DIR` stores catalog snapshots and generated covers. Create it before startup, for example `install -d -m 0700 /srv/nginx-hls/catalog-cache`. `CATALOG_REFRESH_INTERVAL_SECONDS` defaults to `21600` (6 hours). The Dockerfile deliberately fixes NGINX `1.30.5` and Kaltura module commit `26f06877b0f2a2336e59cda93a3de18d7b23a3e2`. Change either only as a deliberate upgrade, then rebuild and run the smoke test. ## Run Start the stack after creating the configured cache directory: ```sh set -a . ./.env set +a install -d -m 0700 "$CATALOG_CACHE_DIR" docker compose --env-file .env up -d --build docker compose ps ``` The catalog validates files during its background refresh and omits invalid MP4s from the published snapshot. Use `./scripts/validate-media.sh "$MEDIA_DIR"` before publishing only when an update must contain no invalid files at all. For a media file at: ```text /srv/media/movies/example.mp4 ``` request this HLS master playlist: ```text http://:8124/hls/movies/example.mp4/master.m3u8 ``` Path segments must be URL encoded by clients. There is intentionally no directory listing or progressive-download route. ## Media Catalog Clients discover validated MP4 files through the same origin: ```text GET /api/media ``` The response is a flat array from the latest completed catalog snapshot: ```json [ { "path": "movies/example.mp4", "name": "example", "playlistUrl": "/hls/movies/example.mp4/master.m3u8", "coverUrl": "/api/media/covers/.jpg" } ] ``` Catalog requests never scan media or run FFmpeg. A background worker scans the tree at startup and then every six hours, validates one H.264 video track plus one AAC audio track, and pre-generates covers for every valid file. It publishes the completed JSON and JPEGs atomically, so clients continue to read the last successful snapshot while a refresh runs or fails. Without any completed snapshot, media and cover requests return `503`. Use the `coverUrl` from the media item to request a pre-generated cover: ```text GET /api/media/covers/.jpg ``` `GET /api/media/status` reports the refresh state, latest successful timestamp, valid and skipped file counts, and the latest build error. Trigger an immediate asynchronous rebuild with `POST /api/media/rescan`; concurrent requests do not start duplicate builds. The service is intended for a trusted LAN, so this endpoint has no additional authentication. Browser caching remains disabled. ```sh curl -sS http://:8124/api/media/status curl -i -X POST http://:8124/api/media/rescan ``` Run the smoke test after the container becomes healthy. Its default test media is `data/aa/1170193193-1-192.mp4`; override it with a relative, URL-encoded `TEST_MEDIA_PATH` when needed. ```sh export PLAYER_ORIGIN=http://192.168.1.20:3000 ./scripts/smoke-test.sh ``` ## Publishing Updates Use a staging directory on the same filesystem as the media library, validate the staged MP4, and atomically rename it into place. Restart the container only after the rename completes: ```sh ./scripts/validate-media.sh /srv/media/.staging mv -T /srv/media/.staging/example.mp4 /srv/media/movies/example.mp4 ./scripts/refresh-vod.sh ``` The service sends `Cache-Control: no-store` and the restart clears VOD caches. This makes a replacement visible promptly, but intentionally interrupts active playback for a short period. Do not overwrite MP4 files in place. ## Operations - Health endpoint: `GET /healthz` - Logs: `docker compose logs -f vod` - Stop: `docker compose down` - The host firewall should limit TCP `8124` to the trusted LAN. This service does not provide TLS or authentication. The first build downloads the fixed source inputs and base image. The ARM64 target host therefore needs access to Docker registries, `nginx.org`, and GitHub during `docker compose build`.