Docker is a good fit for part of a React Native project and the wrong tool for another part. It can run your Metro dev server, your lint and unit tests, and an Android build on a clean Linux image. It cannot build an iOS app, and it cannot run an Android emulator or reach a USB-connected phone without extra work. Most “dockerize React Native” guides skip that line, so this one starts with it.
I created a fresh project with the current CLI (React Native 0.87.1, Node 23) and ran Metro, the Jest test and the TypeScript and ESLint checks on it. The Docker files below are checked with the Dockerfile linter and a YAML parser, but I did not have Docker installed on the machine I wrote this on, so I have not built the images myself. Treat the Android build stage as a well-informed starting point and expect to adjust version numbers.
What Docker Can and Cannot Do for React Native
| Task | In Docker? | Notes |
|---|---|---|
| Metro dev server | Yes | Publish port 8081 and connect the app to it. |
| Lint, type-check, Jest tests | Yes | Plain Node work, ideal for a container. |
| Android APK or AAB build | Yes, with effort | Needs JDK 17, the Android SDK and a large image. |
| iOS build | No | Requires macOS and Xcode, per the React Native environment guide. |
| Android emulator | Not practical | Needs hardware virtualization and a display; run it on the host. |
| USB phone debugging | Awkward | Run adb on the host and forward ports. |
That split decides the design: keep the emulator, Xcode and your phone on the host, and move the repeatable, toolchain-heavy parts (JavaScript checks and Android builds) into containers. If you want to see this wired into automation, the follow-up is CI/CD for Dockerized React Native apps. For a web front end the story is simpler, and I covered it in how to Dockerize a React app.
Prerequisites
- Docker with Compose v2 (the
docker composecommand). - On the host: Node.js 22.11 or newer, because that is what the current React Native template requires, and a way to run the app (Android Studio’s emulator, or a phone). The environment setup guide lists the current requirements, including JDK 17 for Android.
- For iOS: a Mac with Xcode. Docker does not change that.
Create the Project
The old npx react-native init form is no longer how the docs create projects. The current command uses the Community CLI package:
npx @react-native-community/cli@latest init MyApp
cd MyAppIf you would rather use a framework, the React Native docs also point to Expo, which has its own build service and is a different workflow from the one in this post.
Before touching Docker, confirm the project works without it. On my test project these all passed:
npm test -- --watchAll=false
npx tsc --noEmit
npm run lintStarting Metro with npx react-native start printed “Dev server ready” and the Android bundle request returned HTTP 200, so the same command is what the container will run.
Ignore What Should Not Enter the Image
Create a .dockerignore first. Without it, COPY . . drags your host node_modules, iOS Pods and build output into the image, which slows builds and can bring in binaries compiled for the wrong operating system.
node_modules
.git
ios/Pods
ios/build
android/build
android/app/build
android/.gradle
.bundle
*.logOne Dockerfile, Two Targets
A multi-stage Dockerfile lets one file serve both jobs. The shared base stage installs dependencies; metro adds the dev server; android adds Java and the Android SDK. Copying package.json and the lockfile before the rest of the code means Docker reuses the dependency layer until those files change, and npm ci installs exactly what the lockfile says.
# syntax=docker/dockerfile:1
FROM node:24-bookworm-slim AS base
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# Metro dev server, for JavaScript work
FROM base AS metro
EXPOSE 8081
CMD ["npx", "react-native", "start", "--host", "0.0.0.0"]
# Android toolchain, for building an APK on Linux
FROM base AS android
COPY --from=eclipse-temurin:17-jdk-jammy /opt/java/openjdk /opt/java/openjdk
ENV JAVA_HOME=/opt/java/openjdk \
ANDROID_HOME=/opt/android-sdk
ENV PATH="${PATH}:${JAVA_HOME}/bin:${ANDROID_HOME}/cmdline-tools/latest/bin"
# Match these to android/build.gradle (compileSdkVersion, buildToolsVersion, ndkVersion)
ARG ANDROID_PLATFORM=android-37.0
ARG ANDROID_BUILD_TOOLS=37.0.0
ARG ANDROID_NDK=27.1.12297006
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl unzip ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p "${ANDROID_HOME}/cmdline-tools" \
&& curl -fsSL -o /tmp/cmdline-tools.zip \
https://dl.google.com/android/repository/commandlinetools-linux-15859902_latest.zip \
&& unzip -q /tmp/cmdline-tools.zip -d "${ANDROID_HOME}/cmdline-tools" \
&& mv "${ANDROID_HOME}/cmdline-tools/cmdline-tools" "${ANDROID_HOME}/cmdline-tools/latest" \
&& rm /tmp/cmdline-tools.zip
RUN yes | sdkmanager --licenses > /dev/null \
&& sdkmanager "platform-tools" "platforms;${ANDROID_PLATFORM}" \
"build-tools;${ANDROID_BUILD_TOOLS}" "ndk;${ANDROID_NDK}"
WORKDIR /app/android
CMD ["./gradlew", "assembleDebug"]A few choices worth explaining:
- Node 24 and JDK 17. The React Native docs ask for Node 22.11+ and say JDK versions higher than 17 may cause problems, so the Android stage copies a JDK 17 from the official Temurin image instead of using whatever the base image ships.
- No
node:latest. The original version of this post usedlatest, which changes under you. Pin a major version and bump it on purpose. - SDK versions come from your project. The new-project template on my machine asked for compile SDK 37, build-tools 37.0.0 and NDK 27.1.12297006 in
android/build.gradle. I confirmed those packages exist in Google’s SDK repository, where the platform is namedandroid-37.0. Your template will move on, so read your ownandroid/build.gradleand change the threeARGlines to match. Once licenses are accepted, Gradle can also download a missing component by itself. - Command-line tools. The download URL is the Linux zip listed on Android’s command-line tools section; the build number changes, so refresh it from that page when it stops working.
- Apple Silicon. Google’s SDK repository does not list a separate ARM build for Linux, so on an Apple Silicon Mac you may need
--platform linux/amd64and the emulation that comes with it, which makes the Android build slower.
The linter flagged two warnings on this file, both advisory: it would like the apt packages pinned, and it dislikes a pipe in a RUN line without pipefail. I left both, because pinning apt packages breaks as Debian updates them and yes | sdkmanager exits early by design.
Run Metro with Compose
Compose ties the container to your source folder, so edits on the host show up inside it:
services:
metro:
build:
context: .
target: metro
ports:
- "8081:8081"
volumes:
- .:/app
- /app/node_modulesdocker compose up --buildThe .:/app bind mount shares your code. The extra /app/node_modules volume stops that mount from hiding the dependencies installed in the image. Bind mounts can make file watching slower on macOS and Windows; if Metro misses changes, that is the first place to look.
Compose no longer needs a version: line at the top, so it is omitted.
Connect an App to the Containerized Metro
Because the container publishes port 8081, Metro looks like it is running on your host, and the connection steps are the normal React Native ones from the running on a device guide:
- USB device or emulator: run
adb reverse tcp:8081 tcp:8081on the host. This forwards the device’s port 8081 to the host, where Docker forwards it into the container. - Wi-Fi device: put the phone and computer on the same network, open the in-app Dev Menu, set the debug server host and port to your computer’s IP address and 8081, then reload.
To get the app onto the device in the first place you either build on the host (npm run android) or install the APK from the next section.
Run Checks Inside the Container
Because the toolchain is in the image, anyone can run the same checks without installing Node versions locally:
docker compose run --rm metro npm test -- --watchAll=false
docker compose run --rm metro npx tsc --noEmit
docker compose run --rm metro npm run lintThese are the same commands that passed on my host, so only the container wrapper is unverified.
Build an Android APK in Docker
A second Compose file keeps the heavy build separate from daily development. It saves the Gradle cache in a named volume, so later builds do not re-download everything, and mounts the output folder so the APK appears on your host:
services:
android:
build:
context: .
target: android
volumes:
- ./android/app/build:/app/android/app/build
- gradle-cache:/root/.gradle
volumes:
gradle-cache:docker compose -f android-build.yml run --rm --build android
adb install android/app/build/outputs/apk/debug/app-debug.apkExpect the first build to take a long time and the image to be several gigabytes, mostly the SDK, NDK and Gradle downloads. A debug APK expects to load JavaScript from Metro; for a release build you also need signing configuration and secrets, which belong in your CI system, not in an image.
What About iOS?
iOS apps can only be built on a Mac with Xcode, so there is no Dockerfile for it. Your realistic options are building on your own Mac, using a macOS CI runner, or handing the build to a cloud service such as EAS Build. The CI/CD post compares those choices.
Troubleshooting
| Symptom | Likely cause and fix |
|---|---|
| App cannot reach Metro on a USB device | Run adb reverse tcp:8081 tcp:8081 again; it resets when the device reconnects. |
| Port 8081 already in use | Another Metro is running. Stop it, or change the left side of the port mapping, for example "8082:8081", and use the new port on the device. |
| Metro does not notice edits | Slow file events on bind mounts. Check that the .:/app mount is present and, on macOS or Windows, try keeping the project inside the Docker-accessible file share. |
Unsupported class file major version in Gradle | The wrong JDK. React Native expects JDK 17; check java -version inside the android stage. |
SDK location not found or missing platform | The ANDROID_HOME path or the SDK package versions do not match android/build.gradle. |
| Native module changes do not apply | Rebuild the image; a package that changes native code needs a fresh Gradle build, not just a Metro reload. |
Conclusion
Dockerize the parts of React Native that are about tools: the Metro server, your JavaScript checks and your Android build. Keep emulators, iOS builds and USB devices on the host or on proper build machines. That gives teammates one command to start, one command to test, and a reproducible Android build, without pretending a container can do things it cannot.
FAQs
Can I build a React Native iOS app in Docker?
No. iOS builds require macOS and Xcode, and Docker containers on Linux cannot provide them. Use your own Mac, a macOS CI runner or a cloud build service such as EAS Build.
Can I run the Android emulator inside a Docker container?
It is possible only with special setups that need hardware virtualization and a display, and it is rarely worth it. The practical approach is to run the emulator on your host and point it at the Metro server in the container with adb reverse.
Which Node and Java versions does React Native need?
The current React Native documentation asks for Node.js 22.11 or newer and JDK 17, and warns that newer JDKs may cause problems. Check the environment setup guide for your version.
How do I connect a physical phone to Metro running in Docker?
Publish port 8081 from the container, then either run adb reverse tcp:8081 tcp:8081 for a USB-connected Android device, or set the debug server host in the Dev Menu to your computer’s IP address and port 8081 when both are on the same Wi-Fi network.
Why is my React Native Docker image so large?
The Android stage contains Java, the Android SDK, build tools and the NDK, which together take several gigabytes. Use the lightweight metro stage for daily development and build the Android stage only when you need an APK.
Is Docker worth it for a solo React Native developer?
Usually only for the Android build and CI. For day-to-day work, running Metro directly on your machine is simpler and faster. Docker pays off when a team needs identical toolchains or when builds run in CI.








