How to Build Your React Native App Locally (APK and .app)

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.

Claude skill for local builds

If you'd rather have Claude wire this up for you, I turned the workflow into a Code with Beto skill. Install it with npx skills add https://github.com/code-with-beto/skills and pick local-build, it reads your app.config.ts, finds your scheme, and generates both scripts with your real app name baked in.

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.

Going further

Small details like this are the ones that make React Native click, the point where you stop fighting the tooling and start shipping faster. My React Native course goes deep on the fundamentals and the production patterns I use in real apps, so you end up with the mental model, not just the commands.

If you'd rather get practical tips and experiments like this in your inbox, my newsletter is the best place to follow along.

Let's connect!