The STF provider of one Android or iOS device of Zebrunner MCloud: the device-<udid>-stf container that mcloud-agent starts next to the connector, appium and uploader containers of every device. It waits for the STF services (and for usbmuxd and WebDriverAgent of an iOS device), then runs the provider of STF for the device: stf provider for Android, stf ios-device for iOS.
The image is based on the STF image public.ecr.aws/zebrunner/stf (Ubuntu 26.04, zebrunner/stf#1089) of the STF of mcloud (TAG_STF of its .env) and is built for the platform of its base image: stf:3.1 is linux/amd64 and linux/arm64 (zebrunner/stf#1063), so are go-ios and websocat; the build stops when the platform of the base image is not the target one.
Follow the installation and configuration guide in MCloud to reuse this image effectively.
| Component | Version |
|---|---|
| Base image | public.ecr.aws/zebrunner/stf:3.1 (pinned by digest) |
| go-ios | 1.3.2, the version of mcloud-ios-connector; it replaces the go-ios of the base image |
| websocat | 1.13.0, the static musl build of the platform (x86_64 or aarch64) |
socat, curl, procps |
the versions of the Ubuntu release of the base image |
STF runs the go-ios commands ios install, ios launch and ios reboot for an iOS device. go-ios is built from its sources (checked by SHA-256) without cgo by golang:1.26.5-alpine: a static binary of the same version for both platforms, which does not depend on the glibc of the base image.
The image installs the tools of its entrypoint itself, so it does not depend on the ones of the STF base image (the reworked STF image of zebrunner/stf#1052 drops go-ios, socat and curl): socat, curl and procps from the Ubuntu release of the base image.
Everything the build downloads is pinned: the images by version and digest, go-ios and websocat by version and SHA-256 (in the tools build stage). The Ubuntu packages are not pinned: their exact versions disappear from the mirror after an update.
make build VERSION=3.1 # public.ecr.aws/zebrunner/mcloud-device:3.1 in the local docker
make push VERSION=3.1 # built by docker buildx and pushed: docker login public.ecr.aws firstmake build builds the image for the platform of this machine, make push builds linux/amd64 and linux/arm64 (PLATFORMS) with docker buildx and pushes them as one multi-platform image. IMAGE_REPO sets another repository (e.g. IMAGE_REPO=registry.example.com/mcloud-device), VERSION is the tag and the
org.opencontainers.image.version label (dev by default, required by push). make push uses the buildx builder
mcloud (docker-container driver), which it creates once; another platform than the one of the machine needs QEMU
(docker run --privileged --rm tonistiigi/binfmt --install all).
Under emulation (an amd64 image on an Apple Silicon Mac) stf ios-device hangs on its first go-ios command, so the iOS
simulators of a Mac need the arm64 image.
mcloud-agent creates the container (roles/devices/templates/zebrunner-farm, roles/mac-devices/templates/zebrunner-farm), linked to the connector container of the device as connector:
# Android
docker run -d --name device-<udid>-stf --restart on-failure:50 --link device-<udid>-connector:connector \
-e PLATFORM_NAME=android -e DEVICE_UDID=<udid> -e STF_PROVIDER_DEVICE_NAME="Pixel 8" \
-e STF_PROVIDER_NAME=mcloud-linux -e STF_PROVIDER_HOST=agent.example.com \
-e STF_PROVIDER_MIN_PORT=7404 -e STF_PROVIDER_MAX_PORT=7410 -p 7404-7410:7404-7410 \
-e STF_PROVIDER_PUBLIC_IP=stf.example.com -e PUBLIC_IP_PORT=443 -e PUBLIC_IP_PROTOCOL=https \
-e STF_PROVIDER_CONNECT_SUB=tcp://stf.example.com:7250 -e STF_PROVIDER_CONNECT_PUSH=tcp://stf.example.com:7270 \
-e STF_NODE_TOKEN=<node token> -e STF_ZMQ_CURVE_SERVER_PUBLIC_KEY=<key> -e STF_ZMQ_CURVE_SECRET_KEY=<key> \
public.ecr.aws/zebrunner/mcloud-device:<version>
# iOS: the same with
# -e PLATFORM_NAME=ios -e WDA_HOST=connector -e WDA_WAIT_TIMEOUT=60
# -e STF_PROVIDER_CONNECT_APP_DEALER=tcp://stf.example.com:7160 -e STF_PROVIDER_CONNECT_DEV_DEALER=tcp://stf.example.com:7260
# a wireless tvOS device has no connector: -e WIRELESS=true -e WDA_HOST=<host of its WebDriverAgent> -e WDA_PORT=<port>Every variable has its default in the Dockerfile. The entrypoint validates the configuration before anything starts: every invalid variable is logged, then it exits with 0. The numeric variables of the table must be non-negative integers, STF_PROVIDER_MIN_PORT must not be greater than STF_PROVIDER_MAX_PORT, a set STF_PROVIDER_CONNECT_PORT of Android must be a port 1024-65535.
| Variable | Default | Description |
|---|---|---|
PLATFORM_NAME |
android |
android or ios, case-insensitive. |
DEVICE_UDID |
empty | The UDID of the device, required for iOS (--serial of stf ios-device). The provider of Android serves the device of the adb server of STF_PROVIDER_ADB_HOST. |
WIRELESS |
false |
true for an iOS device without a usb path (e.g. an Apple TV reached over a network tunnel of the host): usbmuxd is not forwarded, only WebDriverAgent of WDA_HOST is needed. mcloud-agent sets it for the wireless devices. |
| Variable | Default | Description |
|---|---|---|
STF_PROVIDER_CONNECT_PUSH |
empty | The ZeroMQ PULL endpoint of STF, tcp://<host>:<port>, required. |
STF_PROVIDER_CONNECT_SUB |
empty | The ZeroMQ PUB endpoint of STF, tcp://<host>:<port>, required. |
STF_PROVIDER_CONNECT_APP_DEALER |
empty | The app-side ZeroMQ DEALER endpoint, tcp://<host>:<port>, required for iOS. It is not an option of the provider of Android, so it is removed from its environment. |
STF_PROVIDER_CONNECT_DEV_DEALER |
empty | The device-side ZeroMQ DEALER endpoint, tcp://<host>:<port>, required for iOS, removed for Android as above. |
STF_ZMQ_CURVE_SERVER_PUBLIC_KEY |
empty | The CURVE public key of the triproxies of the STF server (given to the node by its enrollment, ./zebrunner.sh join of mcloud-agent), 64 hex characters, required: STF connects to ZeroMQ over CURVE only (ZeroMQ of STF). |
STF_ZMQ_CURVE_SECRET_KEY |
empty | The CURVE secret key of the device host (mcloud-agent makes it), 64 hex characters, required; the node enrolls with its public key, the STF server lets in the keys of the active nodes only. Neither logged nor traced. |
STF_NODE_TOKEN |
empty | The node token of the device host, mcn1.<label>.<secret> (the secret: 43 base64url characters), required: ./zebrunner.sh join of mcloud-agent enrolls the node and passes its token. STF reads it itself: the provider (and its device processes) and stf ios-device prove with an HMAC of it that their devices belong to the node, and authenticate with it to the storage of STF (the screenshots and the application installs). STF rejects the registration of a device without a valid proof, and a revoked node stops it (node enrollment of STF). A secret: neither logged nor traced by VERBOSE=true, an invalid one is reported without its value. |
STF_SERVICES_WAIT_TIMEOUT |
300 |
How long in seconds the entrypoint waits for the STF services above (TCP connections) before it asks for a restart. |
STF_SERVICES_WAIT_RETRY |
30 |
The delay in seconds between the checks of the unreachable services. |
STF_SERVICES_WAIT_RESTARTS |
2 |
How many restarts in a row the entrypoint asks for (exit 1) when the services stay unreachable, then it exits with 0 and the restarts stop. The counter is kept in /tmp/zebrunner/stf-services-restarts (it survives the restarts of the container) and is reset by reachable services or by the end of the restarts. |
| Variable | Default | Description |
|---|---|---|
STF_PROVIDER_PUBLIC_IP |
localhost |
The public host of STF: the storage URL and the screen WebSocket URLs, required. |
PUBLIC_IP_PORT |
80 |
The public port of STF. |
PUBLIC_IP_PROTOCOL |
http |
http or https, case-insensitive; https makes the screen WebSocket wss. |
The provider of Android reads every STF_PROVIDER_* variable as its option (STF_PROVIDER_MIN_PORT is --min-port), an empty one is removed from its environment, so its default applies. For iOS the entrypoint passes them to stf ios-device as options.
| Variable | Default | Description |
|---|---|---|
STF_PROVIDER_NAME |
empty | The name of the provider in STF, required for iOS; for Android the hostname of the container by default. |
STF_PROVIDER_HOST |
STF_PROVIDER_PUBLIC_IP |
The host of the provider (the farm host): the screen WebSocket path and the adb connect URL unless REMOTE_CONNECT_HOST is set. It must be the address of the node invite (./zebrunner.sh node-invite of mcloud, the address the server connects to): STF rejects the registration of the devices of another host with the token of the node. |
REMOTE_CONNECT_HOST |
STF_PROVIDER_HOST |
The host the users run adb connect <host>:<port> with (the remote debug of STF, --connect-url-pattern), a host name or address without a scheme and a port. With the adb relay of mcloud (ADB_RELAY=true: the device hosts in a private LAN, their device ports forwarded by the MCloud server) it is the public name of MCloud. Not an option of the provider (not STF_PROVIDER_*). |
STF_PROVIDER_DEVICE_NAME |
empty | The name of the device in STF. |
STF_PROVIDER_ADB_HOST |
connector |
Android: the adb server of the connector container. |
STF_PROVIDER_ADB_PORT |
5037 |
Android: the port of the adb server. |
STF_PROVIDER_MIN_PORT |
7404 |
The first port of the device: the screen WebSocket. |
STF_PROVIDER_MAX_PORT |
7410 |
The last port of the device. |
STF_PROVIDER_CONNECT_PORT |
empty | Android: the fixed remote debug port of the device (adb connect <host>:<port>, --connect-port of the provider), 1024-65535; empty: the second port of STF_PROVIDER_MIN_PORT..STF_PROVIDER_MAX_PORT. mcloud-agent sets it from the connect_port setting of the device and publishes the port. Needs an STF image with the --connect-port option of stf provider: the provider of an older one stops on the unknown option. iOS ignores it (--connect-port of stf ios-device is MJPEG_PORT). |
STF_PROVIDER_BOOT_COMPLETE_TIMEOUT |
60000 |
How long in milliseconds to wait for the boot of the device. |
STF_PROVIDER_CLEANUP |
false |
Android: reset the device between the uses (uninstall the applications, reset the accounts). |
STF_PROVIDER_GROUP_TIMEOUT |
3600 |
The timeout in seconds of the automatic release of an inactive device. |
STF_PROVIDER_HEARTBEAT_INTERVAL |
10000 |
The interval in milliseconds of the heartbeat messages. |
STF_PROVIDER_LOCK_ROTATION |
false |
Android: lock the rotation while the device is used. |
STF_PROVIDER_MUTE_MASTER |
never |
Mute the master volume: always, inuse or never. |
STF_PROVIDER_SCREEN_JPEG_QUALITY |
30 |
The JPEG quality of the screen. |
STF_PROVIDER_SCREEN_PING_INTERVAL |
30000 |
The interval in milliseconds of the pings of the screen WebSocket. |
STF_PROVIDER_SCREEN_RESET |
false |
Android: go back to the home screen when the device is released; off not to hide the applications of the automated runs. |
STF_PROVIDER_VNC_INITIAL_SIZE |
600x800 |
Android: the initial size of the VNC server. |
STF_PROVIDER_VNC_PORT |
5900 |
Android: the port of the VNC server. |
BROADCAST_PORT |
2223 |
The TCP port of the screen broadcast (websocat): the screen WebSocket of STF_PROVIDER_MIN_PORT for the video recording of the appium container (BROADCAST_HOST=device). |
ZMQ_TCP_KEEPALIVE |
1 |
The TCP keepalive of ZeroMQ: against the gray screen after 48 hours without a restart (android-device#70, openstf/stf#100). |
ZMQ_TCP_KEEPALIVE_IDLE |
600 |
The idle time in seconds of the ZeroMQ keepalive. |
NODE_TLS_REJECT_UNAUTHORIZED |
0 |
0 accepts a self-signed certificate of the STF storage (the screenshots over https, #56), node warns about it at the start of STF (#203). 1 verifies the certificates and removes the warning: with a self-signed certificate mount its CA into the container and point NODE_EXTRA_CA_CERTS at it. |
| Variable | Default | Description |
|---|---|---|
WDA_HOST |
connector |
The host of WebDriverAgent, the connector container or the host of a wireless device. |
WDA_PORT |
8100 |
The port of WebDriverAgent. |
MJPEG_PORT |
8101 |
The MJPEG port of WebDriverAgent: the screen of stf ios-device (its --connect-port). |
WDA_WAIT_TIMEOUT |
30 |
How long in seconds the entrypoint waits for http://${WDA_HOST}:${WDA_PORT}/status. |
USBMUXD_SOCKET_ADDRESS |
connector:2222 |
usbmuxd of the connector container, <host>:<port>, required unless WIRELESS=true. go-ios reads it itself, the entrypoint also forwards /var/run/usbmuxd to it for the tools that don't. |
USBMUXD_SOCKET_TIMEOUT |
60 |
How long in seconds the entrypoint waits for usbmuxd. |
USBMUXD_SOCKET_PERIOD |
5 |
The delay in seconds between the checks of usbmuxd. |
GO_IOS_AGENT_HOST |
connector |
The host of the go-ios tunnel API (port 60105). The connector container shares its userspace tunnel for iOS newer than 17.4.0, so go-ios in this container (logs, apps, files, network info) reaches iOS 17+ services through it without a tunnel of its own. |
| Variable | Default | Description |
|---|---|---|
LOGGER_LEVEL |
INFO |
The minimum level of the entrypoint logs: DEBUG, INFO, WARN or ERROR. |
DEBUG |
false |
true keeps the container alive for DEBUG_TIMEOUT seconds after the entrypoint exits. |
DEBUG_TIMEOUT |
3600 |
The pause of the debug mode in seconds. |
VERBOSE |
false |
true traces the commands of the entrypoint (set -x), but the checks of the secrets (STF_NODE_TOKEN, the CURVE keys): their values are not traced. |
- The configuration is validated (see Configuration).
- The STF services are awaited: push, sub and for iOS the dealers. Every unreachable one is checked again every
STF_SERVICES_WAIT_RETRYseconds, up toSTF_SERVICES_WAIT_TIMEOUTseconds. - iOS:
/var/run/usbmuxdis forwarded toUSBMUXD_SOCKET_ADDRESS(socat, skipped withWIRELESS=true), then WebDriverAgent is awaited. - The screen WebSocket is broadcast to
BROADCAST_PORT; websocat is started again a second after it exits. - STF starts:
stf providerfor Android,stf ios-devicefor iOS. The entrypoint passesSIGTERMofdocker stopto STF, so it stops gracefully, and exits with the exit code of STF. For iOS the status of WebDriverAgent is logged after STF (#184).
The image runs as stf, the user of the base image: the forwarded usbmuxd socket is /tmp/usbmuxd/usbmuxd, /var/run/usbmuxd is a symlink to it.
- The main repository zebrunner/stf is being reworked (
sync/upstream-master): a new base image may change the options ofstf providerandstf ios-device. Dependabot does not update the base image; after a manual updatemake dockerstarts both commands with the options of the entrypoint and fails on an unknown option. STF_PROVIDER_CONNECT_PUSH,STF_PROVIDER_CONNECT_SUBand the dealers are single endpoints.
0: a problem a restart does not fix: an invalid configuration (every invalid variable is logged), or STF services unreachable afterSTF_SERVICES_WAIT_RESTARTSrestarts in a row. Also0when STF stops gracefully (e.g.docker stop).1: a restart may fix it: unreachable STF services (up toSTF_SERVICES_WAIT_RESTARTStimes in a row), usbmuxd or WebDriverAgent of an iOS device not available.- Otherwise the exit code of STF.
- The healthcheck of the image is the STF process (
pgrep -f "stf (provider|ios-device)"), mcloud-agent shows the health state of the container. - Set
DEBUG=trueto keep the container alive forDEBUG_TIMEOUTseconds after the entrypoint exits, andLOGGER_LEVEL=DEBUGorVERBOSE=truefor more output.
make help lists the targets, make check runs all of them as CI does (every step runs, the failed ones fail the target):
| Target | What it checks |
|---|---|
make lint |
tests/lint.sh: yamllint, actionlint, hadolint, pymarkdown, bash -n, ShellCheck and shfmt (style of .editorconfig), every env var of the Dockerfile documented in this README, the versions of the Dockerfile in Image contents, the text file checks of CONVENTIONS.md. The tools are installed into .venv from tests/requirements-lint.txt; hadolint comes from brew install hadolint on macOS. |
make test |
The static checks of the repository (tests/repo_test.sh: the pins and the env vars of the Dockerfile, the Dependabot configuration) and the tests of util/logger.sh, util/debug.sh and entrypoint.sh (tests/entrypoint_test.sh): configuration validation, the wait for the STF services and its restarts, usbmuxd, WebDriverAgent, the options and the environment of stf provider and stf ios-device, the exit codes and SIGTERM. The tools are stubs (stf, socat, curl, websocat, ...), so no docker, no STF and no device are needed, but bash 4+ is (on macOS brew install bash) and python3. |
make docker |
tests/image_test.sh: builds the image (PLATFORM, linux/amd64 by default, IMAGE overrides its tag, REBUILD=true rebuilds it), checks its contents, configuration, configuration validation, the restart of unreachable services, and starts STF of Android and iOS against fake STF services, adb, usbmuxd and WebDriverAgent: health, the broadcast port, usbmuxd as a non-root user, a graceful docker stop. |
Every test script prints its checks and writes JUnit XML into JUNIT_DIR when it is set.
The GitHub Actions workflow .github/workflows/ci.yml runs the same lint, test and docker jobs on pushes and pull requests to develop, main and master.
Dependabot (.github/dependabot.yml) opens weekly pull requests for the GitHub Actions. The base image, go-ios and websocat are updated by hand: the version, the digest or the SHA-256 in the Dockerfile and the Image contents.
The conventions of the style, the formatting and the tooling of this repository are in CONVENTIONS.md, the same in all the MCloud repositories.
Feel free to support the development with a donation for the next improvements.
