Inside the image
How docker-webstation installs the broker, templates nginx, and starts it as an s6 service.
This page is for anyone maintaining the image or debugging a container that will not come up. Nothing here needs configuring in normal use.
How the broker gets in
The Dockerfile's runtime stage installs the broker from a GitHub release tarball, so nothing has to be attached to a release for the image to consume it:
ARG BROKER_RELEASE
# ...
echo "**** install broker ****" && \
mkdir -p /tmp/broker && \
if [ -z ${BROKER_RELEASE+x} ]; then \
BROKER_RELEASE=$(curl -sX GET "https://api.github.com/repos/romm-streaming/romm-broker/releases/latest" \
| jq -er '.tag_name'); \
fi && \
curl -o \
/tmp/broker.tar.gz -L \
"https://github.com/romm-streaming/romm-broker/archive/${BROKER_RELEASE}.tar.gz" && \
tar xf \
/tmp/broker.tar.gz -C \
/tmp/broker/ --strip-components=1 && \
pip install /tmp/broker --break-system-packages && \
cd /tmp/broker/frontend && \
npm install && \
SUBFOLDER=/streaming/ npm run build && \
mkdir -p /usr/share/webstation-broker && \
cp -r dist /usr/share/webstation-broker/wwwThree things fall out of that:
- The Python package is pip-installed system wide, which registers the
webstation-brokerconsole script the service runs. - The frontend is built once, at image build time, with
SUBFOLDER=/streaming/baked into the vite base. Outside dev mode the broker serves thatdistas static files fromBROKER_FRONTEND_DIST(default/usr/share/webstation-broker/www). ChangingSUBFOLDERat runtime still works for the API and nginx, but the built asset paths keep the prefix they were built with; run a different prefix through a rebuild, or through dev mode, where vite serves the page live. BROKER_RELEASEpins the tag. Unset, the build takes the latest GitHub release. linuxserver's pipeline sets it from the release it detected (see below), so a published image always corresponds to one tag and the version pip records inside the image matches it.
How a release reaches the image
docker-webstation's romm branch is configured with
external_type: github_stable against romm-streaming/romm-broker. A
scheduled GitHub Actions workflow (external_trigger_scheduler.yml) runs the
external trigger, which:
- Reads the latest release tag from the broker's GitHub releases.
- Reads the
build_versionlabel off the currently publishedghcr.io/linuxserver/webstation:rommmanifest. - If the two differ and no build is already running, kicks the Jenkins
pipeline, which builds the image with
BROKER_RELEASEset to the new tag and publishes it.
So cutting a broker release (see Releases) is all it takes
for a new image to appear; there is no manual step on the image side. The
image's own README.md and package_versions.txt are regenerated by the
pipeline.
Boot sequence
The image is built on baseimage-selkies, so the desktop, nginx, PulseAudio
and selkies are the base image's services. The romm branch adds two s6
units:
init-romm-config
Runs after init-selkies-config and before init-selkies-end:
# ensure master token is set
if [ -z "${SELKIES_MASTER_TOKEN}" ]; then
SELKIES_MASTER_TOKEN=$(openssl rand -hex 24)
echo "[ls.io-init] Generated SELKIES_MASTER_TOKEN for this boot"
fi
printf "%s" "${SELKIES_MASTER_TOKEN}" > /run/s6/container_environment/SELKIES_MASTER_TOKEN
# handle subfolder pathing
SFOLDER="${SUBFOLDER:-/streaming/}"
SFOLDER="/${SFOLDER#/}"
SFOLDER="${SFOLDER%/}/"
printf "%s" "${SFOLDER}stream/" > /run/s6/container_environment/SELKIES_SUBFOLDER
printf "%s" "http://127.0.0.1:${CUSTOM_WS_PORT:-8082}${SFOLDER}stream" > /run/s6/container_environment/SELKIES_CONTROL_URL
# modify nginx ports for broker
NGINX_CONFIG=/etc/nginx/sites-available/default
if [[ "${BROKER_DEV_MODE,,}" == "true" ]]; then
sed -i "s/BROKERWEB/5173/g" ${NGINX_CONFIG}
else
sed -i "s/BROKERWEB/8000/g" ${NGINX_CONFIG}
fiSELKIES_MASTER_TOKENis the bearer token the broker uses to push token maps to selkies. Generated per boot unless supplied, and exported into the container environment so both services see the same value.- selkies is moved under
SUBFOLDER+stream/, andSELKIES_CONTROL_URLtells the broker where its control plane answers. The broker tries<control url>/api/tokensfirst and/tokenssecond, remembering whichever accepted the last push. - The nginx template's
BROKERWEBplaceholder becomes the port the room page is served from: 8000 (uvicorn serving the built frontend) in normal mode, 5173 (the vite dev server) in dev mode.
svc-broker
Depends on init-services and svc-selkies. It waits for the selkies
control plane, then either execs the installed console script or, in dev mode,
installs the mounted source editable and runs uvicorn and vite with reload:
echo "[svc-broker] waiting for selkies control plane at ${SELKIES_CONTROL_URL}"
until curl -sf -o /dev/null "${SELKIES_CONTROL_URL}/api/status"; do
sleep 1
done
if [[ "${BROKER_DEV_MODE,,}" == "true" ]]; then
pip install -e /broker --break-system-packages
cd /broker/frontend
s6-setuidgid abc npm install
s6-setuidgid abc npm run dev -- --host 127.0.0.1 --port 5173 &
cd /broker
exec s6-setuidgid abc \
uvicorn --factory webstation_broker.app:create_app \
--host 127.0.0.1 --port 8000 \
--reload --reload-dir /broker/webstation_broker
else
exec s6-setuidgid abc webstation-broker
fiThe broker runs as the abc user (PUID/PGID), the same user the desktop
and the emulators run as, so everything under /config has one owner. On
start it reaps any emulator left behind by a previous broker process, using
the pid record at BROKER_PID_FILE.
nginx layout
/defaults/default.conf is templated with SUBFOLDER and serves everything
under that one prefix, on 3000 (HTTP) and 3001 (HTTPS, self-signed):
| Location | Upstream | Purpose |
|---|---|---|
SUBFOLDER + stream/ | static files | the selkies web client |
SUBFOLDER + stream/api | selkies (CUSTOM_WS_PORT, default 8082) | the stream websocket and selkies control API, unlimited body size |
SUBFOLDER + api/ | broker on 8000 | the REST API, 512 MB body limit for state files and save archives |
SUBFOLDER + ws/ | broker on 8000 | the collab room websocket |
SUBFOLDER | BROKERWEB (8000 or 5173) | the room page and its assets |
Every proxied location forwards Host, X-Forwarded-For and
X-Forwarded-Proto, which is what lets activate derive the callback origin
from the request. Websocket locations carry the upgrade headers and hour-long
timeouts so an idle session is not culled.
Files the broker writes under /config
| Path | What |
|---|---|
/config/broker-emulator.json | pid record of the running emulator (BROKER_PID_FILE) |
/config/broker-exports/ | save archives exit could not upload, or every archive in dev mode (BROKER_EXPORT_DIR) |
/config/broker-imports/ | save archives uploaded through the import route, ready for activate (BROKER_IMPORT_DIR) |
/config/<emulator>.log | each launcher's captured stdout and stderr, *_LOG_PATH |
/config/.XDG/pcsx2.sock | the PINE socket the PCSX2 launcher drives |