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

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 assembleRelease

The output lands at:

android/app/build/outputs/apk/release/app-release.apk

Install it on a connected device or emulator with:

adb install -r android/app/build/outputs/apk/release/app-release.apk

That'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 \
  -quiet

A few things worth pointing out:

  • -sdk iphonesimulator: this builds a .app bundle for the simulator. If you want a real device build, you'd target iphoneos and 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. Check ios/YourApp.xcodeproj/xcshareddata/xcschemes/ to confirm.

The output ends up at:

ios/build/Build/Products/Release-iphonesimulator/YourApp.app

A .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.identifier

Wrapping 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

Want more tips like this? Join the newsletter. One email a week.

Let's connect!