In one of my current projects, we used Create React App to set up a React project.

Our build pipeline is built around Docker containers, the build runs in a container on our Buildkite agent, and the output is a Docker image. This keeps the agent nice and clean, as all build tools and dependencies are kept inside the containers, except for a small number of glue scripts. The quite large size of Docker images does, however, bring some maintenance issues to clean up images from older builds, or you'll quickly run out of disk space on your build agent.

Due to cascading dependencies npm install installs more than a thousand npm packages, several hundred megabytes in size, and we don't actually need the packages, npm or node in the finished image. Deleting data from a Docker image during build merely hides the files in Dockers layered file system, and squashing the image layers invalidates the build cache feature.

Before mid-2017, solving this problem would likely have meant running several builds, bouncing some artifacts to the host in between, but a much simpler solution (introduced in Docker 17.05) is now available with Docker Multistage Builds.

Our current (single) Dockerfile now looks like this;

# the first build stage (named 'cibuild') is based on node:6,
FROM node:6 AS cibuild
# Tell npm (react-scripts) we are running in a CI environment,
# which runs all tests and fails the build on linting failures
ENV CI=true
# Please remove a couple of thousand lines of info-log messages
# from npm output
ENV NPM_CONFIG_LOGLEVEL error
# Enable docker build cache for 'npm install' by only
# including files required for the install
COPY package.json ./
# and run the install
RUN npm install
# then add public resources and application source
COPY public public
COPY src src
# run tests and create an optimized production build
RUN npm test && npm run build
# then add a second build stage based on nginx
# for serving static content
FROM nginx:1.13
# take the output from the cibuild stage and place it at
# nginx's default location for static content
COPY --from=cibuild /build /usr/share/nginx/html
# and expose nginx default port
EXPOSE 80

We don't even need a custom nginx configuration file, as we place the files at the default location.

The resulting image is a nice and simple nginx image (about 128MB) to serve our static content and only the build output is fetched from the large node image (about 1.4GB). A container based on the nginx image is then placed behind a proxy that will send requests for static content (the app itself) to this nginx-container, and the React app's requests to a backend service running in a different container.