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 won't start. Nothing here needs configuring in normal use.
How the broker gets in
The Dockerfile's runtime stage (the final stage of the image build) installs the broker from a GitHub release tarball. That's the source code archive GitHub generates automatically for every release, so nobody has to manually attach a file to a release for the image to use 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 installed system-wide with pip (Python's package
manager), which registers the
webstation-brokerconsole script the service runs. - The frontend (the room's web page) is built once, when the image is
built, with
SUBFOLDER=/streaming/baked into it by vite, the tool that builds the page's files. Outside dev mode, the broker serves those built files as static files fromBROKER_FRONTEND_DIST(default/usr/share/webstation-broker/www). You can still changeSUBFOLDERafterward for the API and nginx, but the built page keeps whatever prefix it was built with. To use a different prefix, rebuild the image, or use dev mode, where vite serves the page live instead of from a pre-built copy. BROKER_RELEASEpins the tag. If it's not set, the build uses the latest GitHub release. linuxserver's pipeline sets it to whatever release it found (more on that below), so every published image corresponds to exactly 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, pointed at romm-streaming/romm-broker. A
scheduled GitHub Actions workflow (external_trigger_scheduler.yml) runs and
does the following:
- 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, it starts the Jenkins
pipeline, which builds the image with
BROKER_RELEASEset to the new tag and publishes it.
So publishing a broker release (see Releases) is all it
takes for a new image to appear. There's no manual step on the image side. The
pipeline also regenerates the image's own README.md and
package_versions.txt.
Boot sequence
The image is built on baseimage-selkies, so the desktop, nginx, PulseAudio
and selkies are already running as services from the base image. The romm
branch adds two more, as s6 units (s6 is the process supervisor linuxserver
images use to start and manage services):
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 authenticate when it pushes token maps to selkies. It's generated fresh each boot unless you set it yourself, and it's shared with both services through the container environment so they always agree on the same value.- selkies runs under
SUBFOLDER+stream/, andSELKIES_CONTROL_URLis the base address the broker uses to reach it, both for status checks and for pushing token maps. The broker tries<control url>/api/tokensfirst, then/tokens, and remembers whichever one worked for next time. - The nginx template's
BROKERWEBplaceholder is filled in with whichever port serves the room page: 8000 in normal mode (uvicorn, the web server that runs the broker, serving the built frontend) or 5173 in dev mode (the vite dev server).
svc-broker
This service starts after init-services and svc-selkies. It waits for
selkies to answer, then either runs the installed broker command, or, in dev
mode, installs the mounted source code and runs uvicorn and vite with
auto-reload instead:
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, using whatever PUID and PGID you set.
That's the same user the desktop and the emulators run as, so everything
under /config has one owner. When it starts, the broker also cleans up any
emulator process a previous broker left running, using the pid record at
BROKER_PID_FILE.
nginx layout
/defaults/default.conf is nginx's config file. SUBFOLDER gets filled into
it at boot, and it serves everything under that one prefix, on port 3000
(HTTP). It also listens on port 3001 (HTTPS, self-signed), which you can ignore: use
port 3000 only.
| 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 the Host, X-Forwarded-For and
X-Forwarded-Proto headers. That's what lets activate work out the callback
origin from the incoming request. Websocket locations also carry the upgrade
headers and hour-long timeouts, so an idle session doesn't get disconnected.
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 |