Expo's cloud build service is the easy way to produce Android and iOS release builds, but build minutes are limited, and queues get long at the worst moments. For Android you can run the exact same build on your own machine with eas build --local. Here is the setup we use for Perron, an Expo app, on ordinary Linux machines.
The toolchain
Local builds need the Android toolchain that matches your Expo and React Native versions. For Expo SDK 54 with React Native 0.81, that was:
| Component | Version |
|---|---|
| JDK | 17 |
| Android SDK platform | API 36 |
| Build tools | 36.0.0 |
| NDK | 27.1.12297006 |
| CMake | 3.22.1 |
Install the Android command-line tools, accept the licences with yes | sdkmanager --licenses, install the components above, and set ANDROID_HOME. When you upgrade Expo, check the matching versions again; a mismatched NDK is the most common cause of confusing native build errors.
Run it from inside the workspace
In a pnpm monorepo, this matters more than anything else: run the build with pnpm dlx eas-cli build --local from the app's folder, not with npx eas-cli.
npx runs outside the pnpm workspace. When EAS then calls expo config to read your app's configuration, it cannot resolve the workspace packages and fails with a terse "exited with non-zero code: 1". pnpm dlx runs inside the workspace and resolves everything correctly. It is also exactly what our CI does, so local and CI builds behave the same.
The build still talks to Expo's servers to fetch the signing keystore and auto-increment the version code, so set an EXPO_TOKEN for a robot account rather than your own login.
Give Gradle enough memory, or limit it
Gradle is hungry. On a machine with only a few gigabytes free, the build dies halfway with "Gradle build daemon disappeared unexpectedly", which is the out-of-memory killer at work. Two fixes, best together:
- Add swap. A generous swap file turns a crash into a slower build.
- Cap Gradle's parallelism in
~/.gradle/gradle.properties:
org.gradle.jvmargs=-Xmx2048m
org.gradle.workers.max=2
kotlin.daemon.jvmargs=-Xmx1024m
With those settings, a release build that used to crash finishes reliably: around 16 minutes on a constrained machine, closer to 8 to 10 with plenty of memory.
Is it worth it?
For teams that ship often, yes. Local builds are free, start immediately, and give you full logs when something fails. Keep the cloud service for iOS, which needs a Mac, or for the odd build when your machines are busy. And write the toolchain versions down next to the project, so the next upgrade does not start with an afternoon of archaeology.