Splitting MediaRTX from Apache in docker-bundle
All checks were successful
tests / ci (8.5) (push) Successful in 4m0s
linter / quality (push) Successful in 1m16s
tests / ci (8.4) (push) Successful in 1m36s

This commit is contained in:
2026-05-20 00:59:19 +03:30
parent cb62a0bae6
commit b3bb160b90
11 changed files with 66 additions and 48 deletions

View File

@@ -9,6 +9,7 @@ PUBLIC_SCHEME=https
APP_DOMAIN=nyone.example.com
HLS_DOMAIN=nyone-hls.example.com
APP_HTTP_PORT=80
HLS_HTTP_PORT=127.0.0.1:8888
RTMP_PUBLIC_PORT=1935
APP_NAME=Nyone

View File

@@ -14,14 +14,14 @@ This folder is a self-contained deployment bundle. A server only needs the `dock
Default ports use the services' normal defaults:
- HTTP app/HLS proxy: `80`
- HTTP app proxy: `80`
- RTMP ingest: `1935`
- MediaMTX HLS inside Docker: `8888`
- MediaMTX HLS on localhost: `8888`
- MediaMTX API inside Docker: `9997`
- PostgreSQL inside Docker: `5432`
- Redis inside Docker: `6379`
Only HTTP and RTMP are published by default. HLS is served through Apache on `HLS_DOMAIN`, not directly from MediaMTX.
HTTP and RTMP are published by default. HLS is published only on `127.0.0.1:8888` by default so host Nginx can proxy `HLS_DOMAIN` directly to MediaMTX without exposing the HLS port publicly.
## Server Requirements
@@ -33,7 +33,7 @@ Only HTTP and RTMP are published by default. HLS is served through Apache on `HL
- DNS for both `APP_DOMAIN` and `HLS_DOMAIN`.
- External TLS termination through a reverse proxy, load balancer, or CDN.
If a host-level reverse proxy already uses port `80`, set `APP_HTTP_PORT` to another local port and forward both hostnames to that port.
If a host-level reverse proxy already uses port `80`, set `APP_HTTP_PORT` to another local port for the app and keep `HLS_HTTP_PORT` on a local MediaMTX port for HLS.
## First Deployment
@@ -78,6 +78,7 @@ Important values:
- `APP_DOMAIN`: Laravel app hostname.
- `HLS_DOMAIN`: HLS playback hostname. Must differ from `APP_DOMAIN`.
- `APP_HTTP_PORT`: host port published by Apache, default `80`.
- `HLS_HTTP_PORT`: host address published by MediaMTX HLS, default `127.0.0.1:8888`.
- `RTMP_PUBLIC_PORT`: host RTMP port, default `1935`.
- `PUBLIC_SCHEME`: usually `https` behind external TLS.
- `SEED_DATABASE`: defaults to `false`; set to `true` only if you intentionally want the app seeder to run.
@@ -136,7 +137,7 @@ Do not remove volumes unless you intend to delete database, Redis, and stored ap
## External Proxy Notes
Forward both `APP_DOMAIN` and `HLS_DOMAIN` to the Apache container's published HTTP port. Preserve the `Host` header and set `X-Forwarded-Proto: https` when TLS is terminated outside Docker.
Forward `APP_DOMAIN` to the Apache container's published HTTP port. Forward `HLS_DOMAIN` directly to the MediaMTX HLS localhost port. Preserve the `Host` header for the Laravel app and set `X-Forwarded-Proto: https` when TLS is terminated outside Docker.
Expose TCP `1935` publicly for OBS/RTMP ingest. Do not expose MediaMTX API port `9997` publicly.
@@ -146,13 +147,14 @@ Use this pattern when Nginx runs on the host and handles public HTTP/HTTPS. In t
```env
APP_HTTP_PORT=127.0.0.1:8080
HLS_HTTP_PORT=127.0.0.1:8888
PUBLIC_SCHEME=https
APP_DOMAIN=app.example.com
HLS_DOMAIN=hls.example.com
RTMP_PUBLIC_PORT=1935
```
Run `./scripts/deploy.sh` after changing `.env`, then point Nginx at `http://127.0.0.1:8080`. Replace the example domains and certificate paths before enabling the config.
Run `./scripts/deploy.sh` after changing `.env`, then point Nginx app traffic at `http://127.0.0.1:8080` and HLS traffic at `http://127.0.0.1:8888`. Replace the example domains and certificate paths before enabling the config.
```nginx
# /etc/nginx/sites-available/nyone-docker-bundle.conf
@@ -203,7 +205,11 @@ server {
ssl_certificate_key /etc/letsencrypt/live/hls.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://127.0.0.1:8888;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -215,6 +221,10 @@ server {
proxy_request_buffering off;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
add_header Access-Control-Allow-Headers "Range, Origin, Accept, Content-Type" always;
add_header Access-Control-Expose-Headers "Content-Length, Content-Range" always;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
}
}
@@ -229,3 +239,15 @@ sudo systemctl reload nginx
```
Keep RTMP separate from this HTTP proxy. Open TCP `1935` to the Docker host so OBS can publish directly to MediaMTX.
## HLS CORS Check
Host Nginx owns the public HLS CORS headers. MediaMTX is configured with `hlsAllowOrigins: []` so the response does not include duplicate `Access-Control-Allow-Origin` headers after proxying.
Check the deployed headers with the exact app origin:
```bash
curl -I -H "Origin: https://app.example.com" "https://hls.example.com/channel/index.m3u8"
```
The response should contain one `Access-Control-Allow-Origin` header matching the app origin. If two `Access-Control-Allow-Origin` headers appear, rebuild the MediaMTX container from this bundle and reload Nginx.

View File

@@ -11,7 +11,7 @@ description: Work on the Nyone docker-bundle deployment bundle under docker-bund
- Treat `docker-bundle/.env` and `docker-bundle/runtime/` as server-local generated state; do not commit or depend on them.
- Do not make this bundle depend on the cloned app repository's `.env` or `deploy/mediamtx.yml`.
- Keep MediaMTX config Docker-owned at `docker/mediamtx/mediamtx.yml.template`.
- Keep `APP_DOMAIN` and `HLS_DOMAIN` separate because Apache uses separate virtual hosts.
- Keep `APP_DOMAIN` and `HLS_DOMAIN` separate because the app and HLS are served by different host-level Nginx virtual hosts.
- Do not run `scripts/deploy.sh`, `docker compose up`, or image builds unless the user confirms this is a deployment server.
## Architecture
@@ -21,19 +21,20 @@ description: Work on the Nyone docker-bundle deployment bundle under docker-bund
- `docker-compose.yml` defines `app`, `db`, `redis`, `mediamtx`, `queue`, `scheduler`, `viewer-sync`, and one-shot `migrate`.
- `docker/app/Dockerfile` builds the Laravel Apache image from `runtime/source`.
- `docker/app/entrypoint.sh` selects behavior by `CONTAINER_ROLE`.
- `docker/app/apache/nyone.conf.template` serves Laravel on `APP_DOMAIN` and proxies HLS for `HLS_DOMAIN`.
- `docker/app/apache/nyone.conf.template` serves Laravel on `APP_DOMAIN`.
- Host Nginx should proxy `HLS_DOMAIN` directly to the locally published MediaMTX HLS port.
- `docker/mediamtx/entrypoint.sh` renders the MediaMTX template at container start.
## Port Defaults
- Apache published HTTP: `80`.
- MediaMTX RTMP: `1935`.
- MediaMTX HLS internal: `8888`.
- MediaMTX HLS local publish/internal: `8888`.
- MediaMTX API internal: `9997`.
- PostgreSQL internal: `5432`.
- Redis internal: `6379`.
Only publish HTTP and RTMP by default. Keep MediaMTX HLS behind Apache and keep the API private on the Docker network.
Publish Apache HTTP and RTMP by default, and bind MediaMTX HLS to `127.0.0.1:8888` for host Nginx. Keep the MediaMTX API private on the Docker network.
## Editing Guidance
@@ -51,9 +52,11 @@ Only publish HTTP and RTMP by default. Keep MediaMTX HLS behind Apache and keep
## Nginx TLS Proxy Guidance
- Document Nginx as a host-level TLS reverse proxy in `README.md`; do not add Nginx as another Compose service unless the user asks.
- Keep Docker Apache as the upstream for both app and HLS hostnames. Nginx should proxy both domains to the published Apache port and preserve the `Host` header so Apache can select the correct virtual host.
- Keep Docker Apache as the upstream for the app hostname only. Nginx should proxy HLS directly to the locally published MediaMTX HLS port.
- Recommend `APP_HTTP_PORT=127.0.0.1:8080` when Nginx owns host ports `80` and `443`.
- Recommend `HLS_HTTP_PORT=127.0.0.1:8888` so HLS is reachable by host Nginx but not publicly exposed as a raw port.
- Include `X-Forwarded-Proto https`, `X-Forwarded-Port 443`, `X-Forwarded-Host`, `X-Real-IP`, and `X-Forwarded-For` in proxy examples.
- Let host Nginx own public HLS CORS headers. Keep `hlsAllowOrigins: []` in the MediaMTX template so proxied HLS responses do not contain duplicate `Access-Control-Allow-Origin` values.
- Keep HLS proxy examples buffering off and cache headers set to no-store.
- Keep RTMP outside the HTTP proxy guidance unless explicitly using Nginx stream proxying; the default is direct TCP `1935` to MediaMTX through Docker.

View File

@@ -8,6 +8,7 @@ It assumes:
- HLS playback domain: `hls.nyone.app`
- RTMP ingest URL: `rtmp://nyone.app:1935`
- Docker Apache is private on `127.0.0.1:8080`
- Docker MediaMTX HLS is private on `127.0.0.1:8888`
- Host Nginx handles public HTTP, HTTPS, and SSL certificates
The application repository itself does not need to exist on the server before deployment. The deploy script clones it into `docker-bundle/runtime/source`.
@@ -56,7 +57,7 @@ ufw --force enable
ufw status
```
Docker can publish container ports outside some UFW rules. In this deployment, Apache is bound to `127.0.0.1:8080`, while RTMP is intentionally public on `1935`.
Docker can publish container ports outside some UFW rules. In this deployment, Apache is bound to `127.0.0.1:8080`, MediaMTX HLS is bound to `127.0.0.1:8888`, and RTMP is intentionally public on `1935`.
## 4. Install Docker Engine and Compose plugin
@@ -135,6 +136,7 @@ PUBLIC_SCHEME=https
APP_DOMAIN=nyone.app
HLS_DOMAIN=hls.nyone.app
APP_HTTP_PORT=127.0.0.1:8080
HLS_HTTP_PORT=127.0.0.1:8888
RTMP_PUBLIC_PORT=1935
APP_NAME=Nyone
@@ -329,7 +331,11 @@ server {
ssl_certificate_key /etc/letsencrypt/live/nyone.app/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://127.0.0.1:8888;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -341,6 +347,10 @@ server {
proxy_request_buffering off;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
add_header Access-Control-Allow-Origin "https://nyone.app" always;
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
add_header Access-Control-Allow-Headers "Range, Origin, Accept, Content-Type" always;
add_header Access-Control-Expose-Headers "Content-Length, Content-Range" always;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
}
}
@@ -385,6 +395,14 @@ Run one viewer-count sync pass:
docker compose --env-file .env -f docker-compose.yml exec app php artisan streaming:sync-viewer-counts --once -v
```
Check HLS CORS headers:
```bash
curl -I -H "Origin: https://nyone.app" "https://hls.nyone.app/<channel-slug>/index.m3u8"
```
The response should include exactly one `Access-Control-Allow-Origin: https://nyone.app` header. A `404` can be normal when the channel is not currently publishing, but duplicate `Access-Control-Allow-Origin` headers mean both Nginx and an upstream are setting CORS; rebuild the MediaMTX container from this bundle and reload Nginx.
Check that MediaMTX API is not exposed on the host. This should fail because the API is only available inside the Docker network:
```bash

View File

@@ -59,7 +59,6 @@ x-app-environment: &app-environment
MEDIAMTX_SHARED_SECRET: ${MEDIAMTX_SHARED_SECRET:-}
APP_DOMAIN: ${APP_DOMAIN:-nyone.example.com}
HLS_DOMAIN: ${HLS_DOMAIN:-nyone-hls.example.com}
HLS_INTERNAL_PORT: 8888
APP_OPTIMIZE_ON_BOOT: ${APP_OPTIMIZE_ON_BOOT:-true}
SEED_DATABASE: ${SEED_DATABASE:-false}
@@ -142,13 +141,12 @@ services:
dockerfile: docker/mediamtx/Dockerfile
restart: unless-stopped
environment:
APP_URL: ${PUBLIC_SCHEME:-https}://${APP_DOMAIN:-nyone.example.com}
HLS_PUBLIC_URL: ${PUBLIC_SCHEME:-https}://${HLS_DOMAIN:-nyone-hls.example.com}
MEDIAMTX_SHARED_SECRET: ${MEDIAMTX_SHARED_SECRET:-}
RTMP_INTERNAL_PORT: 1935
HLS_INTERNAL_PORT: 8888
ports:
- "${RTMP_PUBLIC_PORT:-1935}:1935"
- "${HLS_HTTP_PORT:-127.0.0.1:8888}:8888"
volumes:
- app-storage:/var/www/html/storage
depends_on:

View File

@@ -33,7 +33,7 @@ RUN apt-get update \
zip \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& a2enmod headers proxy proxy_http rewrite setenvif \
&& a2enmod headers rewrite setenvif \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

View File

@@ -22,22 +22,3 @@ ServerName ${APP_DOMAIN}
ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 combined
</VirtualHost>
<VirtualHost *:80>
ServerName ${HLS_DOMAIN}
ProxyRequests Off
ProxyPreserveHost On
ProxyPass "/" "http://mediamtx:${HLS_INTERNAL_PORT}/" retry=0
ProxyPassReverse "/" "http://mediamtx:${HLS_INTERNAL_PORT}/"
Header always set Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate"
Header always set Access-Control-Allow-Origin "${APP_URL}"
Header always set Access-Control-Allow-Methods "GET, HEAD, OPTIONS"
Header always set Access-Control-Allow-Headers "Range, Origin, Accept, Content-Type"
ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 combined
</VirtualHost>

View File

@@ -41,9 +41,9 @@ artisan() {
}
render_apache_config() {
export APP_DOMAIN HLS_DOMAIN HLS_INTERNAL_PORT APP_URL
export APP_DOMAIN
envsubst '${APP_DOMAIN} ${HLS_DOMAIN} ${HLS_INTERNAL_PORT} ${APP_URL}' \
envsubst '${APP_DOMAIN}' \
< /etc/apache2/sites-available/nyone.conf.template \
> /etc/apache2/sites-available/000-default.conf
}

View File

@@ -10,16 +10,14 @@ if [ -z "${MEDIAMTX_SHARED_SECRET:-}" ] || [ "${MEDIAMTX_SHARED_SECRET#change-me
fail "MEDIAMTX_SHARED_SECRET is required. Run docker-bundle/scripts/deploy.sh or set it in docker-bundle/.env."
fi
: "${APP_URL:?APP_URL is required.}"
: "${HLS_PUBLIC_URL:?HLS_PUBLIC_URL is required.}"
: "${RTMP_INTERNAL_PORT:=1935}"
: "${HLS_INTERNAL_PORT:=8888}"
mkdir -p /config /var/www/html/storage/app/private/recordings
export APP_URL HLS_PUBLIC_URL MEDIAMTX_SHARED_SECRET RTMP_INTERNAL_PORT HLS_INTERNAL_PORT
export MEDIAMTX_SHARED_SECRET RTMP_INTERNAL_PORT HLS_INTERNAL_PORT
envsubst '${APP_URL} ${HLS_PUBLIC_URL} ${MEDIAMTX_SHARED_SECRET} ${RTMP_INTERNAL_PORT} ${HLS_INTERNAL_PORT}' \
envsubst '${MEDIAMTX_SHARED_SECRET} ${RTMP_INTERNAL_PORT} ${HLS_INTERNAL_PORT}' \
< /etc/mediamtx/mediamtx.yml.template \
> /config/mediamtx.yml

View File

@@ -16,9 +16,7 @@ rtmpAddress: :${RTMP_INTERNAL_PORT}
hls: yes
hlsAddress: :${HLS_INTERNAL_PORT}
hlsVariant: mpegts
hlsAllowOrigins:
- ${APP_URL}
- ${HLS_PUBLIC_URL}
hlsAllowOrigins: []
pathDefaults:
record: yes
@@ -32,4 +30,3 @@ pathDefaults:
paths:
all_others:
source: publisher

View File

@@ -158,7 +158,7 @@ require_config_value APP_DOMAIN "nyone.example.com"
require_config_value HLS_DOMAIN "nyone-hls.example.com"
if [ "$(env_value APP_DOMAIN)" = "$(env_value HLS_DOMAIN)" ]; then
fail "APP_DOMAIN and HLS_DOMAIN must be different because this deployment uses separate Apache virtual hosts."
fail "APP_DOMAIN and HLS_DOMAIN must be different because this deployment uses separate app and HLS hostnames."
fi
GIT_REF="${GIT_REF:-main}"