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

Three things fall out of that:

  • The Python package is pip-installed system wide, which registers the webstation-broker console 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 that dist as static files from BROKER_FRONTEND_DIST (default /usr/share/webstation-broker/www). Changing SUBFOLDER at 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_RELEASE pins 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:

  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, kicks the Jenkins pipeline, which builds the image with BROKER_RELEASE set 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}
fi
  • SELKIES_MASTER_TOKEN is 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/, and SELKIES_CONTROL_URL tells the broker where its control plane answers. The broker tries <control url>/api/tokens first and /tokens second, remembering whichever accepted the last push.
  • The nginx template's BROKERWEB placeholder 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
fi

The 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):

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

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