webstation-broker
Container

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/www

Three things fall out of that:

  • The Python package is installed system-wide with pip (Python's package manager), which registers the webstation-broker console 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 from BROKER_FRONTEND_DIST (default /usr/share/webstation-broker/www). You can still change SUBFOLDER afterward 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_RELEASE pins 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:

  1. Reads the latest release tag from the broker's GitHub releases.
  2. Reads the build_version label off the currently published ghcr.io/linuxserver/webstation:romm manifest.
  3. If the two differ, and no build is already running, it starts the Jenkins pipeline, which builds the image with BROKER_RELEASE set 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}
fi
  • SELKIES_MASTER_TOKEN is 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/, and SELKIES_CONTROL_URL is the base address the broker uses to reach it, both for status checks and for pushing token maps. The broker tries <control url>/api/tokens first, then /tokens, and remembers whichever one worked for next time.
  • The nginx template's BROKERWEB placeholder 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
fi

The 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.

LocationUpstreamPurpose
SUBFOLDER + stream/static filesthe selkies web client
SUBFOLDER + stream/apiselkies (CUSTOM_WS_PORT, default 8082)the stream websocket and selkies control API, unlimited body size
SUBFOLDER + api/broker on 8000the REST API, 512 MB body limit for state files and save archives
SUBFOLDER + ws/broker on 8000the collab room websocket
SUBFOLDERBROKERWEB (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

PathWhat
/config/broker-emulator.jsonpid 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>.logeach launcher's captured stdout and stderr, *_LOG_PATH
/config/.XDG/pcsx2.sockthe PINE socket the PCSX2 launcher drives

On this page