Containerising your projects with Docker simplifies the development experience and facilitates straightforward deployment to cloud environments. Let’s look at how we can package a React site as a Docker container.
This article focuses on projects that have been bootstrapped using create-react-app (CRA). If you’ve ejected your CRA configuration, or are using a custom build process, you’ll need to adjust the npm run build command accordingly.
Docker images are created via a Dockerfile. This defines a base image to use, such as the Apache web server. You then list a series of commands which add packages, apply config changes and copy in files needed by your application.
Defining Our Requirements
CRA includes a built-in live build and reload system, which you access via npm run start. This enables you to quickly iterate on your site during development.
When moving to production, you need to compile your static resources using npm run build. This produces minified HTML, CSS and JavaScript bundles in your build directory. These files are what you upload to your web server.
A basic approach to Dockerising could be to npm run build locally. You’d then copy the contents of the build directory into your Docker image – using a web server base image – and call it a day.
This approach doesn’t scale well, particularly when building your Docker image within a CI enviroment. Your app’s build process isn’t completely encapsulated within the container build, as it’s dependent on the external npm run build command. We’ll now proceed with a more complete example where the entire routine runs within Docker.
A Dockerfile For CRA
This Dockerfile incorporates everything needed to fully containerise the project. It uses Docker’s multi-stage builds to first run the React build and then copy the output into an alpine Apache server container. This ensures the final image is as small as possible.
The first section of the file defines the build stage. It uses the official Node.js base image. The package.json and package-lock.json files are copied in. npm ci is then used to install the project’s npm packages. ci is used instead of install because it forces an exact match with the contents of package-lock.json.
Once the dependencies are installed, the public and src directories are copied into the container. The folders are copied after the npm ci command because they’re likely to change much more frequently than the dependencies. This ensures the build can take full advantage of Docker’s layer caching – the potentially expensive npm ci command won’t be run unless the package.json or package-lock.json files change.
The last step in this build stage is to npm run build. CRA will compile our React app and place its output into the build directory.
The second stage in the Dockerfile is much simpler. The httpd:alpine base image is selected. It includes the Apache web server in an image which weighs in at around 5MB. The compiled HTML, CSS and JavaScript is copied out of the build stage container and into the final image.
Using The Docker Image
Use the docker build command to build your image:
This builds the image and tags it as my-react-app:latest. It uses the Dockerfile found in your working directory (specified as .).
The build may take a few minutes to complete. Subsequent builds will be faster, as layers like the npm ci command will be cached between runs.
Once your image has been built, you’re ready to use it:
Docker will create a new container using the my-react-app:latest image. Port 8080 on the host (your machine) is bound to port 80 within the container. This means you can visit http://localhost:8080 in your browser to see your React project! The -d flag is present so the container runs in the background.
Switching to NGINX
The example above uses Apache but you can easily switch to NGINX instead.
You can adopt alternative web servers in a similar manner; as CRA produces completely static output, you have great flexibility in selecting how your site is hosted. Copy the contents of the /build/build directory from the build stage into the default HTML directory of your chosen server software.
Benefits of This Approach
Using Docker to not only encapsulate your final build, but also to create the build itself, gives your project complete portability across environments. Developers only need Docker installed to build and run your React site.
More realistically, this image is ready to use with a CI server to build images in an automated fashion. As long as a Docker environment is available, you can convert your source code into a deployable image without any manual intervention.
By using multi-stage builds, the final image remains streamlined and should be only a few megabytes in size. The much larger node image is only used during the compilation stage, where Node and npm are necessary.