Enhanced Appium docker image for testing on Android and iOS devices: phones, tablets, Android TV, Apple TV (tvOS), Android emulators and iOS simulators.
- It is built on top of Appium and supports:
- iOS devices of Linux and macOS hosts (usbmuxd of the connector container), wireless iOS and tvOS devices and the iOS / tvOS simulators of a macOS host
- Integration with STF for remote manual testing
- Cached local storage to minimize download calls to external resources (S3, AppCenter, Azure, etc.)
- Video recording of every session, including Apple TV and Android TV
- Video recording for pre-conditions and mapping with test results in Zebrunner Reporting
Feel free to support the development with a donation for the next improvements.
Follow the installation and configuration guide in MCloud to reuse this image effectively.
| Component | Version |
|---|---|
| Base image | appium/appium:v3.1.1-p0 (Appium 3.1.1) |
| UiAutomator2 driver | 6.7.15 |
| XCUITest driver | 10.18.2 |
images plugin, appium-image stage |
4.1.0 (with sharp 0.34.5) |
The Appium base driver is patched for MCloud (files/patch): recording markers, the shared application storage, the device status endpoints and the STF device updates.
make build VERSION=3.1.1 # public.ecr.aws/zebrunner/appium:3.1.1, the `appium` stage
make build VERSION=3.1.1-img TARGET=appium-image # plus the Appium images plugin and sharp
make push VERSION=3.1.1 # built by docker buildx and pushed: docker login public.ecr.aws firstThe image is linux/amd64 only: the build of another platform is rejected, on Apple Silicon it is built emulated.
IMAGE_REPO sets another repository, 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.
In MCloud every device has four containers, created by mcloud-agent (roles/*devices/templates/zebrunner-farm is the reference of all the options):
connector: the adb server (Android) or usbmuxd and WebDriverAgent (iOS) of the devicedevice: the STF provider and the screen broadcast of the deviceappium: this imageuploader: uploads the artifacts of/tmp/logto S3
The appium container reaches the others by the host names connector and device (--link or network aliases) and shares two volumes: the application storage of the host and the artifacts volume of the device with the uploader.
# Android
docker run -d --name device-pixel-appium --net mcloud --restart on-failure:5 \
--link device-pixel-connector:connector --link device-pixel:device \
-v appium-storage-volume:/opt/appium-storage -v device-pixel:/tmp/log \
-e PLATFORM_NAME=android -e DEVICE_UDID=<udid> -e DEVICE_NAME=Pixel \
-e CONNECT_TO_GRID=true -e SELENIUM_HOST=<hub host> -e SELENIUM_PORT=4444 -e STF_PROVIDER_HOST=<host ip> \
-e DEFAULT_CAPABILITIES=true -e RELAXED_SECURITY=true \
-e STF_API_URL=<stf url>/api/v1 -e STF_AUTH_TOKEN=<token> \
-e APPIUM_PORT=4723 -p 4723:4723 \
public.ecr.aws/zebrunner/appium:<tag>
# iOS: the same with
# -e PLATFORM_NAME=ios -e WDA_HOST=connector -e APPIUM_CLI=--session-override
# a simulator of the macOS host: -e WIRELESS=true -e DEVICE_CONNECTION=ios-sim -e HOST_SSH_USER=<user of the host>
# -v <private ssh key>:/opt/zebrunner/ssh/id_ed25519:roThe container is configured with environment variables; the defaults are the values of the image. In MCloud they are set by mcloud-agent.
| Variable | Default | Description |
|---|---|---|
PLATFORM_NAME |
android or ios (any case): the device module to run, the default capabilities and the video source |
|
DEVICE_UDID |
the udid of the device: capabilities, /status-adb, STF updates |
|
DEVICE_NAME |
the device name in the node config and the default capabilities | |
ANDROID_DEVICES |
/status-adb looks for this name when DEVICE_UDID is empty |
PLATFORM_VERSION, DEVICETYPE (Phone, Tablet, TV) and AUTOMATION_NAME are detected by the device module at startup.
| Variable | Default | Description |
|---|---|---|
APPIUM_PORT |
4723 |
the port of Appium, the base path is /wd/hub |
PATH, JAVA_HOME, ANDROID_HOME, TZ |
the ones of the base image | the platform-tools and build-tools of the Android SDK in /opt/android, Java 17 of apksigner, UTC; the other variables of the base image (versions, the paths of its build) are not kept |
APPIUM_CLI |
extra Appium arguments, e.g. --session-override |
|
APPIUM_PLUGINS |
the plugins to use (--use-plugins), images in the appium-image stage |
|
NODE_PATH |
appium-image stage: /opt/appium-extra/node_modules:${APPIUM_HOME}/node_modules |
where Node finds sharp (installed apart from Appium) for the images plugin |
LOG_LEVEL |
info |
the Appium log level |
RELAXED_SECURITY |
true enables all insecure features (--relaxed-security) for every session: mobile: shell (adb_shell), the Appium log of the earlier sessions of the device (get_server_logs), the emulator console, the screen streaming, the download of chromedriver; prefer ADB_SHELL and CHROMEDRIVER_AUTODOWNLOAD for the features the tests need |
|
CHROMEDRIVER_AUTODOWNLOAD |
true |
allows *:chromedriver_autodownload (mobile web on Android) |
ADB_SHELL |
true allows *:adb_shell (mobile: shell) |
|
DEFAULT_CAPABILITIES |
true passes the capabilities of the device (udid, connector hosts and ports) to every session; nothing is passed for other platforms than android and ios |
|
APPIUM_CONFIG_DIR |
/opt/config |
the generated nodeconfig.json and defaultcapabilities.json |
| Variable | Default | Description |
|---|---|---|
CONNECT_TO_GRID |
true registers Appium in mcloud-grid with a generated node config (MobileRemoteProxy) |
|
SELENIUM_HOST, SELENIUM_PORT |
the hub, required with CONNECT_TO_GRID=true |
|
STF_PROVIDER_HOST |
the host of the node url, required with CONNECT_TO_GRID=true |
|
NODE_REGISTRATION_TOKEN |
the secret the node registers with, NODE_REGISTRATION_TOKEN of mcloud-grid: a hub with a token registers no node without it; it is in custom of nodeconfig.json and masked in the node config the container logs |
|
ADB_PORT, PROXY_PORT, SERVER_PROXY_PORT |
5037, 8080, 0 |
the ports of the device in the node capabilities |
UNREGISTER_IF_STILL_DOWN_AFTER |
3000 |
ms, the node config value; after Appium exits with 101 (hub down) the container waits this long plus 15 s before the restart |
A missing required variable or a wrong number ends the container with the reason in its log. CUSTOM_NODE_CONFIG is ignored: the Zebrunner node config is always generated.
With NODE_REGISTRATION_TOKEN (the token of mcloud-grid) Appium answers only the requests with it in the header X-MCloud-Node-Token, which the hub sends, and the ones of the container itself (the healthcheck): the others get 401, their WebSockets (e.g. the logcat broadcast) are closed, the refused address is logged once in 10 minutes. So the device is used through the hub only, which reserves it in STF; Appium Inspector and the other clients connect to the hub too.
Remote applications are kept in a shared storage, so the devices of a host download an application once.
| Variable | Default | Description |
|---|---|---|
APPIUM_APPS_DIR |
/opt/appium-storage |
the storage, an application is stored as <host>/<path> of its url without the query (an http(s) url only; its dot segments are resolved, the file stays in the storage); empty disables the storage |
APPIUM_APP_SIZE_DISABLE |
false |
true uses a stored application without comparing its size with the remote one |
APPIUM_APP_WAITING_TIMEOUT |
600 |
s, how long a session waits for the download of the same application by another device (.lock file) |
APPIUM_MAX_LOCK_FILE_LIFETIME |
1800 |
s, an older .lock file is removed as left by a crashed download |
APPIUM_APP_FETCH_RETRIES |
3 |
the retries of a broken download, 0 disables them |
APPIUM_APPS_MAX_AGE |
7 |
days, a download removes the applications not used for this time; 0 keeps them |
APPIUM_APPS_MIN_FREE |
10 |
%, a download removes the least recently used applications while the free space of the storage is below it; 0 disables it |
Every build of an application is usually a new url, so the storage is cleaned before a download: the applications not used for APPIUM_APPS_MAX_AGE days, then the least recently used ones while the free space is below APPIUM_APPS_MIN_FREE, and the temporary copies of the interrupted downloads. An application is used when a session takes it from the storage; the ones used in the last hour and the ones being downloaded (.lock) are kept. A failed cleanup is logged and does not fail the session.
Every session is recorded: the patched Appium creates ${LOG_DIR}/.recording-artifact-<session id> on a new session and deletes it on the session end. The video, the session log and the other files of LOG_DIR are then moved to ${LOG_DIR}/<session id>/ and ${LOG_DIR}/.artifact-<session id> is created for the uploader. docker stop finishes the active sessions the same way.
| Variable | Default | Description |
|---|---|---|
RECORD_ARTIFACTS |
true |
false disables the video, the logs are still shared |
LOG_DIR |
/tmp/log |
the artifacts, a volume shared with the uploader |
TASK_LOG |
/tmp/log/appium.log |
the Appium log, shared as the session log |
LOG_FILE |
session.log |
the name of the shared session log |
BROADCAST_HOST, BROADCAST_PORT |
device, 2223 |
Android: the STF screen broadcast, switched on and off around the recording |
FFMPEG_OPTS |
extra ffmpeg output options | |
ROUTER_UUID |
the marker name instead of the session id, the id of an external orchestrator of the device (e.g. on-demand emulators) | |
WDA_LOG_FILE, SHARE_WDA_LOG |
/tmp/log/wda.log, false |
iOS: share the WDA log with the session artifacts |
A session with the appium:mcloudExternalRecording: true capability creates no marker: the recording is managed by the client.
| Variable | Default | Description |
|---|---|---|
STF_API_URL |
the STF API, e.g. https://stf.example.com/api/v1 |
|
STF_AUTH_TOKEN |
the STF access token |
With all of STF_API_URL, STF_AUTH_TOKEN and DEVICE_UDID set, the device errors of Appium are reported to STF: the device is marked Unhealthy (no space left on the host or on the device, INSTALL_FAILED_INSUFFICIENT_STORAGE, unfinished device setup, failed am command) or gets a note (another failed install - a problem of the application, e.g. INSTALL_FAILED_VERSION_DOWNGRADE -, lost proxy connection, unknown session).
| Variable | Default | Description |
|---|---|---|
REMOTE_ADB_HOST |
connector |
the adb server of the device container, the default remoteAdbHost capability |
ADB_SERVER_SOCKET |
tcp:connector:5037 |
the adb server for the adb client of the container |
CHROMEDRIVER_PORT |
8200 |
the chromedriver port, forwarded to the connector |
ANDROID_DEVTOOLS_PORT |
9222 |
the Chrome DevTools port, forwarded to the connector |
CHROME_OPTIONS |
"androidDevToolsPort": 9222 |
the members of the default chromeOptions capability; androidDevToolsPort needs Chrome / WebView 87+ on the device (see Limitations) |
| Variable | Default | Description |
|---|---|---|
WDA_HOST, WDA_PORT |
connector, 8100 |
WebDriverAgent, the default webDriverAgentUrl capability |
MJPEG_PORT |
8101 |
the WDA MJPEG server, the video source |
DEVICE_TIMEOUT |
300 |
s, how long the startup waits for WDA |
USBMUXD_SOCKET_ADDRESS |
connector:2222 |
usbmuxd of the connector; the iOS tools use it directly, /var/run/usbmuxd is forwarded to it |
WIRELESS |
false |
true for a device without usbmuxd (a simulator, or WDA started by another tool and relayed by the connector): no usbmuxd forwarding, WDA is reached at WDA_HOST:WDA_PORT |
DEVICE_CONNECTION |
empty | ios-sim for an iOS or tvOS simulator of the macOS host, see Simulators; android-sim for an Android emulator: a device of the default characteristics is a Phone, not a TV |
HOST_SSH_USER |
empty | the user of the macOS host of the simulator |
HOST_SSH_HOST, HOST_SSH_PORT |
connector, 22 |
ssh of the macOS host, relayed by the connector |
HOST_SSH_KEY |
/opt/zebrunner/ssh/id_ed25519 |
the mounted private ssh key of the user of the host |
A simulator runs on the macOS host, its connector boots it and starts WebDriverAgent, Appium of the container drives it as a simulator. XCUITest needs Xcode for a simulator: /usr/local/bin/xcrun of the image runs xcrun of the host over ssh (through the connector, HOST_SSH_HOST:HOST_SSH_PORT), the local files of its arguments (the app to install, ...) are copied to the host first and the files it writes into the temporary folder are copied back. DEVELOPER_DIR points at a copy of Info.plist of the Xcode of the host (the Xcode version). The default capabilities have appium:isHeadless: the container can't open the Simulator UI, a simulator booted with its UI is used as is. Without HOST_SSH_USER the xcrun of the image fails like a missing tool, as before. The features of XCUITest that write the files of the simulator directly (the Safari, keyboard and localization preferences, the app data reset) are not available: their errors are logged and the session goes on.
| Variable | Default | Description |
|---|---|---|
DEBUG |
false |
true keeps the container for DEBUG_TIMEOUT seconds (3600) after the entrypoint exits |
VERBOSE |
false |
true traces the entrypoint (set -x) |
LOGGER_LEVEL |
INFO |
the minimum level of the log lines of the scripts: DEBUG, INFO, WARN or ERROR ([dd/mm/YYYY HH:MM:SS] [LEVEL] message, util/logger.sh); the Appium log level is LOG_LEVEL |
GET /wd/hub/status-adb and GET /wd/hub/status-wda report whether the device or WDA is ready. With a JSON body {"exitCode": <code>} (as mcloud-grid sends) Appium exits with the code when the check fails, so the container restarts. WDA gets 3 s to answer and a second attempt after 500 ms (a WDA busy with the end of the last session is not down), adb devices gets 5 s; mcloud-grid waits 10 s for the check.
- The image is
linux/amd64only: the base imageappium/appiumis amd64 only (Android platform-tools), a build for another platform stops with an error. An ownlinux/arm64image would lose functions: Google publishes no chromedriver (Chrome for Testing) for linux/arm64, so the Chrome and WebView sessions of Android would not work, and no Android build-tools (aapt2,zipalign) either, so they would be older builds of the distribution. On Apple silicon it runs emulated: install Rosetta 2 and let Docker Desktop use it (Use Rosetta for x86_64/amd64 emulation on Apple Silicon), otherwise it runs under the much slower QEMU emulation;./zebrunner.sh statusof mcloud-agent shows it. - Android 8.0+ only (the minimum of the UiAutomator2 driver of the image,
MIN_ANDROIDoffiles/module/android.sh): older devices get a warning at startup and their sessions fail. - Chrome and the WebView contexts need Chrome and Android System WebView 87+ on the device: they are reached through the fixed DevTools port (
androidDevToolsPort), which older chromedrivers reject withunrecognized chrome option: androidDevToolsPort. An older Chrome or WebView gets a warning at startup; update it on the device. - Appium runs as
androidusr, not root, and gets no root: the image removessudoand the sudo group ofandroidusr(a passwordless sudoer in the base image). - The base image is squashed into one layer without the files the image does not use: its own drivers (espresso, flutter, gecko, uiautomator2 6.1.1, about 410 MB) in
~/.appium(Appium loads the drivers ofAPPIUM_HOME), the Androidcmdline-tools(sdkmanager,avdmanager,apkanalyzer: the drivers use adb and the build-tools), the caches of npm and node-gyp, and the files its layers deleted or replaced: 2.41 GB -> 1.68 GB, a download of ~680 MB instead of ~1 GB. The squashed layer is built again by every release, so a device host downloads the whole image on an update.
session not created: please close '<package>' and try againon a switch to aWEBVIEW_<package>context: the WebView belongs to another app than theappPackageof the session (e.g.org.chromium.webview_shellon an AOSP emulator). Appium passes theappPackageto chromedriver as the package it expects in the DevTools, and chromedriver finds<package>there. Setappium:chromeAndroidPackageto the package of the WebView, e.g."appium:chromeAndroidPackage": "org.chromium.webview_shell".- An AOSP emulator has no Chrome: a mobile web session (
browserName: Chrome) fails withEither provide 'app' option to install 'com.android.chrome'.... Use a WebView app (e.g.org.chromium.webview_shell, preinstalled on AOSP) or install Chrome.
-
Clone Zebrunner Appium and copy the patched files into Appium 3.1.1:
git clone https://github.com/zebrunner/appium.git cd appium export APPIUM_HOME="$(npm root -g)/appium" cp -R -v ./files/patch/node_modules "${APPIUM_HOME}/"
files/patch/appium-3_1_1.diffis the same patch for the Appium sources.
The patch is kept twice: files/patch/appium-<version>.diff for the Appium sources (the version of the base image) and its build, the js files of files/patch/node_modules copied into the image. Change the diff (e.g. git diff of a patched appium@<version> checkout), run make patch-build to rebuild the js files and commit both; make patch-sync (also in CI) fails when they differ.
- The docker healthcheck (every 10 s, after a 60 s start period) requires the Appium status; an offline Android device gets
adb reconnectand is unhealthy when it stays offline. - The container exits with
1whenever Appium exits, so theon-failurerestart policy starts it again. Appium exits with101when mcloud-grid finds the device or WDA down (/status-adb,/status-wdawithexitCode): the container then waitsUNREGISTER_IF_STILL_DOWN_AFTERplus 15 s for the hub to unregister the node. - A wrong configuration (e.g.
CONNECT_TO_GRID=truewithout the hub) ends the container with the reason in its log; an iOS container ends when WebDriverAgent does not answer inDEVICE_TIMEOUT. DEBUG=truekeeps an ending container forDEBUG_TIMEOUTseconds to look into it.
make check runs everything CI runs, make help lists the single steps:
| Target | What it checks | Needs |
|---|---|---|
make lint |
yamllint, actionlint, hadolint, pymarkdown, shellcheck, shfmt, node --check, the patch files, the Dockerfile versions in this README and no other Appium version or patch diff mentioned, every env var of the Dockerfile documented, the text file checks of CONVENTIONS.md |
Python 3; on macOS brew install hadolint |
make test |
make build and make push with a docker stub, the config generators files/config/zbr-*.sh, files/util/debug.sh and files/util/logger.sh |
bash, Python 3, jq |
make docker |
the docker tests below: make image, make patch, make e2e and make patch-sync |
Docker |
make image |
the image build and contents, the assumptions on the base image that a new release may break (where Appium is, the entrypoint as PID 1, the user, the Node of the patch build, the base driver version and files the patch replaces, the drivers and plugins using the patched base driver and supporting the Appium version, the default capabilities known to the drivers, the insecure features and the minimum Android of the drivers), the Android and iOS modules and the healthcheck with a fake adb and WDA, Appium startup with the env vars of the image and of mcloud-agent, RECORD_ARTIFACTS=false, shutdown, the appium-image stage with its plugin |
Docker |
make patch |
the Appium patch inside the image with node --test (tests/patch/*.test.mjs): recording markers, /status-wda, /status-adb, STF device updates, the shared app storage |
Docker |
make e2e |
iOS (also wireless) and Android containers with the mcloud-agent settings and a fake device (tests/fixtures/fake-device.js): startup, health, usbmuxd and adb forwarding, video recording and the shared artifacts of sessions, docker stop, the exit code 101 of mcloud-grid |
Docker |
make patch-sync |
the js files of the patch are the build of the patch diff (Appium sources of the base image version, built in node:22) |
Docker |
The docker tests build the image once as zbr-appium:test (IMAGE overrides the tag, REBUILD=true forces a new build, SKIP_BUILD=true skips the build of make image). The image is amd64 only, so on Apple silicon it runs emulated and the docker tests take a while. With JUNIT_DIR set the results are also written as JUnit XML.
The shell scripts are formatted with shfmt (.editorconfig). Formatting-only commits are listed in .git-blame-ignore-revs, GitHub skips them in blame; locally: git config blame.ignoreRevsFile .git-blame-ignore-revs.
The conventions of the style, the formatting and the tooling of this repository are in CONVENTIONS.md, the same in all the MCloud docker image repositories.
