cloud phone in GitLab CI: 2026 pipeline patterns
cloud phone in GitLab CI: 2026 pipeline patterns
cloud phone GitLab CI integration in 2026 is one of the cleanest ways to ship mobile builds without buying a single physical device. you point a .gitlab-ci.yml job at a real Android phone in Singapore over an ADB tunnel, run instrumented tests, capture logs, and release the device when the job ends. the same pipeline that runs your unit tests now runs Espresso, Appium, or Maestro suites on actual hardware. no emulators, no Android Virtual Devices, no flaky local Docker setups.
this guide walks through the patterns that work in production GitLab CI in 2026, including shared runners, self-hosted runners, secret handling, parallel sharding, and what to do when a job times out with a locked device. if you have already read how to integrate cloud phones into your CI/CD pipeline, this post is the GitLab-specific deep dive.
why GitLab CI fits cloud phone testing
GitLab CI gives you three things that map cleanly onto cloud phone workflows. first, every job runs in an isolated container, so the ADB client install does not pollute anything. second, GitLab artifacts handle test reports and screenshots without extra infra. third, the parallel:matrix keyword lets you fan out tests across multiple devices with one block of YAML.
the only piece GitLab does not give you is the device itself. that is what cloud phone providers like cloudf.one solve. you get an authenticated REST API to lock a phone, an ADB endpoint to control it, and a clean unlock path when the job exits.
the building blocks
every GitLab CI mobile pipeline using cloud phones has the same four pieces.
- a runner (shared SaaS runner or self-hosted) that can reach the public internet
- a Docker image with
adb,curl,jq, and your build toolchain (Java, Gradle, Node) - secrets for
CLOUDFONE_TOKENand any signing keys, stored in GitLab CI/CD variables - artifact rules that capture logcat, screenshots, and test XML on failure
if you are running a self-hosted runner inside a private network, make sure outbound HTTPS to the cloud phone API is allowed and that ADB ports are not blocked by your firewall.
a minimal .gitlab-ci.yml for android tests
this is the smallest pipeline that builds a debug APK, locks a cloud phone, runs instrumented tests, and uploads results. it works on any GitLab runner with Docker.
stages:
- build
- test
variables:
CLOUDFONE_API: https://api.cloudf.one/v1
build-apk:
stage: build
image: cimg/android:2026.01-node
script:
- ./gradlew assembleDebug assembleAndroidTest
artifacts:
paths:
- app/build/outputs/apk/
android-tests:
stage: test
image: cimg/android:2026.01-node
needs: [build-apk]
before_script:
- apt-get update && apt-get install -y jq adb
script:
- |
DEVICE=$(curl -s -X POST "$CLOUDFONE_API/devices/lock" \
-H "Authorization: Bearer $CLOUDFONE_TOKEN" \
-d '{"tag":"ci-pool","duration":1800}' | jq -r .device_id)
ADB_ENDPOINT=$(curl -s "$CLOUDFONE_API/devices/$DEVICE/adb" \
-H "Authorization: Bearer $CLOUDFONE_TOKEN" | jq -r .endpoint)
echo "DEVICE=$DEVICE" >> device.env
echo "ADB_ENDPOINT=$ADB_ENDPOINT" >> device.env
adb connect "$ADB_ENDPOINT"
adb -s "$ADB_ENDPOINT" install -r app/build/outputs/apk/debug/app-debug.apk
./gradlew connectedAndroidTest -PandroidTestSerial="$ADB_ENDPOINT"
after_script:
- source device.env || true
- adb -s "$ADB_ENDPOINT" logcat -d > logcat.txt || true
- |
curl -s -X POST "$CLOUDFONE_API/devices/$DEVICE/unlock" \
-H "Authorization: Bearer $CLOUDFONE_TOKEN" || true
artifacts:
when: always
paths:
- logcat.txt
- app/build/reports/androidTests/
reports:
junit: app/build/outputs/androidTest-results/connected/*.xml
the important detail is the after_script block. it always runs, even on failure, and it always unlocks the device. without it, a single panicking job can hold a phone hostage for the full lock duration.
parallel test sharding
once you have one device working, the next move is to parallelize across many. GitLab’s parallel:matrix is the cleanest way.
android-tests:
parallel:
matrix:
- SHARD: ["1/4", "2/4", "3/4", "4/4"]
script:
- |
DEVICE=$(curl -s -X POST "$CLOUDFONE_API/devices/lock" \
-H "Authorization: Bearer $CLOUDFONE_TOKEN" \
-d "{\"tag\":\"ci-pool\",\"duration\":1800}" | jq -r .device_id)
# ... lock, connect, run shard $SHARD ...
four shards, four phones, roughly one quarter of the wall-clock time. cost-wise this is usually a wash because cloud phones bill per minute, but engineers waiting for the build is the line item that actually matters.
handling secrets and signing keys
put CLOUDFONE_TOKEN in GitLab CI/CD variables, marked masked and protected. for signing the release APK, store the keystore as a file-type variable so it lands on disk as $KEYSTORE_PATH without ever showing in logs. the GitLab CI variables docs cover the protected and masked flags in detail.
never commit CLOUDFONE_TOKEN to your repo, even in a .env.example. rotate it through the cloudf.one dashboard every 90 days and update GitLab via the API if you have many projects.
what to do when a job times out with a locked device
this happens. a test hangs, GitLab kills the job at the runner timeout, and the device stays locked until the lock TTL expires. three defenses work well together.
- always set a
durationon the lock that is shorter than your job timeout. if the job is 30 minutes, lock for 25. - always run the unlock call in
after_script, neverscript.after_scriptruns even when the main script crashes. - run a janitor job in your cloudf.one account that scans for locks older than 60 minutes and force-releases them with a friendly Slack ping. you can wire this through Slack notifications so the team sees what got cleaned up.
cost control and runner placement
GitLab shared runners bill per minute, and cloud phones bill per minute, so the temptation is to overprovision both. resist it. three rules keep the spend predictable.
- run instrumented tests only on the merge request and main branch, not on every push to a feature branch. use
rules:ifto gate them. - pick the smallest device pool that covers your screen sizes. testing one Pixel and one mid-range Samsung typically catches 90% of layout bugs.
- co-locate the runner with the device. if your phones are in Singapore, use a self-hosted runner in Singapore or a SaaS runner region close to it. cross-region ADB latency makes test runs 30-40% slower for nothing.
frequently asked questions
can I use cloud phones with GitLab self-hosted runners behind a corporate firewall?
yes, as long as the runner can make outbound HTTPS to the cloud phone API and outbound TCP to the ADB endpoint port range. cloudf.one publishes its IP ranges so your network team can allowlist them.
does GitLab CI support iOS testing the same way?
no, ADB is Android only. for iOS you need a different tunnel pattern and usually a hosted Mac runner. cloudf.one is Android focused. if you need iOS in the same pipeline, run it as a separate job on a different provider.
what happens if my GitLab job is canceled by the user mid-run?
GitLab fires after_script on cancellation in 2026. that means your unlock call still runs. just make sure unlock is in after_script, not in a trailing line of script.
how do I share a device pool between multiple GitLab projects?
create a service account in cloudf.one, generate a token scoped to a single tag like ci-pool, and use that token in every project’s CI variables. all projects then lock from the same pool, and you can size the pool centrally.
can I cache APK artifacts between jobs?
yes, use GitLab’s artifacts:paths to pass the APK from build to test, and cache: for Gradle dependencies. that combo cuts a typical Android build from 8 minutes to under 2 on warm runs.
ready to wire your GitLab pipeline to a real Android phone in Singapore? start a cloudf.one trial and copy the .gitlab-ci.yml above into a fresh project.