Scientyfic World

How to Dockerize a React Native App: What Works, What Does Not

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

Share:

Get an AI summary of this article

Dockerize a react native app banner image

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

TaskIn Docker?Notes
Metro dev serverYesPublish port 8081 and connect the app to it.
Lint, type-check, Jest testsYesPlain Node work, ideal for a container.
Android APK or AAB buildYes, with effortNeeds JDK 17, the Android SDK and a large image.
iOS buildNoRequires macOS and Xcode, per the React Native environment guide.
Android emulatorNot practicalNeeds hardware virtualization and a display; run it on the host.
USB phone debuggingAwkwardRun 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 compose command).
  • 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 MyApp

If 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 lint

Starting 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
*.log

One 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 used latest, 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 named android-37.0. Your template will move on, so read your own android/build.gradle and change the three ARG lines 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/amd64 and 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_modules
docker compose up --build

The .:/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:8081 on 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 lint

These 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.apk

Expect 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

SymptomLikely cause and fix
App cannot reach Metro on a USB deviceRun adb reverse tcp:8081 tcp:8081 again; it resets when the device reconnects.
Port 8081 already in useAnother 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 editsSlow 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 GradleThe wrong JDK. React Native expects JDK 17; check java -version inside the android stage.
SDK location not found or missing platformThe ANDROID_HOME path or the SDK package versions do not match android/build.gradle.
Native module changes do not applyRebuild 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.

Snehasish Konger
Developed @scientyficworld.org | Technical writer @Nected | Content Developer
Connect with Snehasish Konger

On This page

Take a Pause with Intervals

A Sunday letter on building, writing, and thinking deeper as a developer — short, honest, and worth your time.

Snehasish Konger profile photo

"Hey there — I'm Snehasish. Hope this post saved you some head-scratching time! I've spent years turning technical chaos into clarity, and I'm here to be your guide through the maze of modern tech. Stick around for more lightbulb moments — we're just getting started."

Related Posts