How to Build Your React Native App Locally (APK and .app)
Beto, April 2026
Most React Native devs never touch xcodebuild or gradlew directly. You run expo run:ios, it just works, you move on. But once you have the ios/ and android/ folders on disk, you already have everything you need to produce release artifacts with the actual platform toolchains, on your own machine.
This came up recently while I was setting up Maestro e2e tests for Platano. Maestro needs a real .apk or .app file to install on a simulator or emulator, and running a local build was the fastest way to iterate. Turns out it's one command per platform.
Prefer Claude wire this up for you? Install the local-build skill, point it at your app.config.ts, and it generates both scripts with your scheme and app name already filled in.
Install: npx skills add https://github.com/code-with-beto/skills

ToolCode with Beto skills
Claude skills for mobile developers. local-build reads your config and generates these scripts for you.
The dev-client caveat
Before anything else: if you're on Expo, your Debug builds include expo-dev-client. That's the screen with the "Development Servers" list and the "Enter URL manually" button. Great for development, useless for e2e or distributing a build to someone.
For anything where you want the app to launch straight into your actual UI, you need a Release build. This applies to both platforms.
Android: the easy one
From the android/ folder, Gradle does all the work:
cd android
./gradlew assembleReleaseThe output lands at:
android/app/build/outputs/apk/release/app-release.apkInstall it on a connected device or emulator with:
adb install -r android/app/build/outputs/apk/release/app-release.apkThat's it. No signing config needed for local testing, Gradle will use the debug keystore by default if you haven't set up release signing.
iOS: xcodebuild directly
iOS is slightly more verbose because you have to pick a workspace, scheme, and SDK, but it's still one command. From the ios/ folder:
cd ios
xcodebuild \
-workspace YourApp.xcworkspace \
-scheme YourApp \
-configuration Release \
-sdk iphonesimulator \
-derivedDataPath build \
CODE_SIGNING_ALLOWED=NO \
-quietA few things worth pointing out:
-sdk iphonesimulator: this builds a.appbundle for the simulator. If you want a real device build, you'd targetiphoneosand deal with provisioning.CODE_SIGNING_ALLOWED=NO: simulator builds don't need to be signed, but Xcode still tries unless you tell it not to.- Scheme name: by default Expo creates a scheme matching your project's
name. Checkios/YourApp.xcodeproj/xcshareddata/xcschemes/to confirm.
The output ends up at:
ios/build/Build/Products/Release-iphonesimulator/YourApp.appA .app is a directory (not a single file), so you can't just double-click it. You install it into a running simulator:
xcrun simctl boot "iPhone 16" 2>/dev/null
open -a Simulator
xcrun simctl install booted ios/build/Build/Products/Release-iphonesimulator/YourApp.app
xcrun simctl launch booted your.bundle.identifierWrapping it in scripts
You're going to run these over and over, so wrap them. Here's what I use in Platano. Both scripts copy the artifact into builds/ so I always know where to find the latest build.
build-android.sh
#!/usr/bin/env bash
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/.." && pwd)"
ARTIFACT="$ROOT/android/app/build/outputs/apk/release/app-release.apk"
DEST_DIR="$HERE/builds"
DEST="$DEST_DIR/platano.apk"
echo "==> Building Android release APK"
cd "$ROOT/android"
./gradlew assembleRelease
mkdir -p "$DEST_DIR"
rm -f "$DEST"
cp "$ARTIFACT" "$DEST"
echo "Done: $DEST"build-ios.sh
#!/usr/bin/env bash
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/.." && pwd)"
ARTIFACT="$ROOT/ios/build/Build/Products/Release-iphonesimulator/Platano.app"
DEST_DIR="$HERE/builds"
DEST="$DEST_DIR/Platano.app"
echo "==> Building iOS Simulator .app (Release)"
cd "$ROOT/ios"
xcodebuild \
-workspace Platano.xcworkspace \
-scheme Platano \
-configuration Release \
-sdk iphonesimulator \
-derivedDataPath build \
CODE_SIGNING_ALLOWED=NO \
-quiet
mkdir -p "$DEST_DIR"
rm -rf "$DEST"
cp -R "$ARTIFACT" "$DEST"
echo "Done: $DEST"Make them executable once (chmod +x build-*.sh) and you're set. Each run replaces the previous artifact, so Maestro always picks up the latest.
A quick note on EAS
You can of course produce the same artifacts with eas build, and for production releases, code signing, and CI that's usually what you want. This is just the local path when you already have Xcode and the Android SDK set up and you want the build to happen on your own machine, which is handy for e2e flows and quick iteration.
If you already have EAS set up but still need a simulator .app or an .apk locally, there's a middle ground: eas build --platform ios --local (or --platform android --local) runs the exact same EAS build pipeline using your machine as the server. You'll still need a build profile configured for a simulator build or APK output, but you get the EAS workflow without shipping anything to their servers. More details in the Expo docs on local builds.
Going further
These small wins are what make React Native click. You stop fighting the tooling and start shipping.
My React Native course is where I teach the fundamentals and the exact production patterns I use in real apps, so you walk away with the mental model, not just a list of commands.

CourseLearn React Native the right way
Fundamentals to shipping. The concepts behind the prompts, lifetime access, one price.
Want more tips like this? Join the newsletter. One email a week.
Like this article? Get the rest of the library plus weekly React Native tips. Free.