Building React Native in Docker
Published: 2021-01-31 20:08:18


At [In the Pocket](https://www.inthepocket.com/), we recently got a lot more ownership over the app-building process for our team's customers.

Previously, we shared our toolset with all teams. We used Ansible to provision our team-mac server. The server was running our Jenkins instance as well as building our iOS apps. Additionally, we shared a docker image with the company to build Android apps on the Google Cloud.

This dependency of shared tools led to 2 big problems. Firstly, it wasn't always clear who was maintaining what. If a team wanted to upgrade to the latest Android SDK or a newer XCode version, this would have implications for everyone. Secondly, the docker image was bloated by its ability to build React Native, Flutter and native Android apps. It also contained SDK 27, 28 & 29. Simply put: more SDKs and tools meant a slower docker container.

In this situation, every now and then we had building issues that weren't always easy to pinpoint or fix. Or we needed help from colleagues that weren't available for immediate action.

Not so long ago, we moved to our private Gitlab and took the opportunity to use Gitlab CI and polish our toolset! This blog post focuses on what we learned about Docker and building for Android.

---

# Docker-Android?

As we were investing time to migrate our continuous integration from Jenkins to Gitlab CI, we found out there is actually a Docker container maintained by [React Native Community: Docker-Android](https://github.com/react-native-community/docker-android) 🥳!

We happily crafted our .gitlab-ci.yml file, only to find out that bundler (from Ruby) wasn't pre-installed 🤕. A first 'quick fix' was installing it on every run. Unfortunately, it was a waste of time and only slowed down every build. At that point, we really started digging into the matter!

## How to test?

Changing your .gitlab-ci.yml file, commiting & pushing changes is a slow process to see if your app is building correctly in docker.

So what can you do? You can actually build, run & log into the docker container quite easily! Once that's over, you can set up git and checkout your project, or just copy it from your pc.

### Start a docker container

First you have to build your Dockerfile:

```
docker build - tag itporbit/react-native-android:latest .
```

Now we can start it. The option - ti will make it interactive and - rm will stop the container once you exit the terminal.

```
docker run --rm -ti itporbit/react-native-android
```

### Checkout, install & build

Now you are logged in the docker container you can do a git checkout, npm install & build your app!

```
git clone -b develop --singe-branch https://github.com/repo.git
cd app/
npm ci
cd android
./gradlew assembleRelease
```

# Our own Dockerfile ❤️

At this point we have 2 options.

We could fork the docker-android project, and adapt the dockerfile to our needs. That means we would have to merge upstream changes when they occur.

Instead, we opted to create our own Dockerfile and extend from reactnativecommunity/react-native-android:2.1. This way we don't depend on another git repo. We are in control of which version we extend and when we upgrade to a new release.

Now we are ready to make some changes!

## Add support for Fastlane

Our first Dockerfile set some locale stuff and installed bundler.

```
FROM reactnativecommunity/react-native-android:2.1
# set locale to utf8 for fastlane
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
# install bundler
RUN gem install bundler
```

As we use Fastlane, we don't use ./gradlew but:

```
bundle install
bundle exec fastlane build_android
```

## Non-root user

The react-native-android container runs with a root user, which comes in handy to extend from the container but poses a security risk. Therefore, we added a user (reactnative) and switched the workdir to home/reactnative.

```
RUN useradd -ms /bin/bash reactnative
# transfer ownership of $ANDROID_HOME (opt/android) so our new user can install additional sdk or build tools at build time. This is needed if you use libraries that target other Android SDK's.
RUN chown reactnative:reactnative $ANDROID_HOME -R
USER reactnative
WORKDIR /home/reactnative
```

## Versioning is important!

Years ago, when we were still building apps on a self-managed build server, we definitely lost the ability to build older apps when we upgraded the software. By creating our own react-native-android docker container, every version is tailor-made for building a specific Android version. Version 1 builds React Native apps that target Android 10 (SDK 29). When we upgrade React Native to a new version that targets Android 11, we will update release version 2, made for building Android 11 (SDK 30) apps!

Thanks to this, we always target a specific version and not the latest tag in our .gitlab-ci file. When we have to do a hotfix, we will always build our app in the correct container 🎉.

## Make

We use a simple make task so you don't have to remember or document the docker commands to build, publish or run our container.

```
all: build
VERSION = 1.1


build:
  docker build --tag itporbit/react-native-android:${VERSION} .


push: build
  docker push itporbit/react-native-android:${VERSION}
  git tag react-native-android/${VERSION} HEAD
  git push --tags


run: build
  docker run --rm -ti itporbit/react-native-android:${VERSION}
```

So we can now invoke make run to test stuff out after making changes to our Dockerfile!

# Resources and failing builds on mac

While testing, every now and then my docker image started failing unexplainably while building. Apt could no longer install dependencies for example. This wasn't the case, but my reserved disk space was depleted. An easy fix, by running docker builder prune or docker image prune in the terminal so that caching of past builds was removed.

# Interesting reads

* https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
* https://nickjanetakis.com/blog/best-practices-when-it-comes-to-writing-docker-related-files