If you want to use Docker to host your React app, you need to create a Docker image with a production build of your app and a web server to serve it. Here is a sample Dockerfile that does just that. The React app itself is also built within docker so you don't need to worry about setting up a Node.js environment or creating a production build manually.

# syntax=docker/dockerfile:1
FROM node:18-alpine as builder
WORKDIR /home/node/app
COPY . .
RUN npm ci
RUN npm run build

FROM nginx:1.24-alpine as server
COPY --from=builder /home/node/app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

This Dockerfile relies on a custom nginx.conf file and a custom .dockerignore file.

server {
listen 80;
listen [::]:80;

location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}
node_modules
dist

The Dockerfile and .dockerignore files assume that the build output folder is dist. Change dist in both files if your project uses a different build output folder.

To start and use this Dockerfile, use the following commands.

$ docker build -t react-app .
$ docker run -p 8000:80 react-app

To get a better idea of how this Dockerfile works, let's walk through each instruction.

A sketch of a piece of paper with Docker whale and the React icon

Docker syntax version

# syntax=docker/dockerfile:1

#syntax is a parser directive that instructs docker to use the latest stable version of the docker syntax. Explicitly specifying this allows us to use newer versions of the docker syntax without updating Docker itself.

Builder stage

This Dockerfile uses the multi-stage builds feature of docker to use a separate image to create a production build of the React app. This image will not be part of the final output and will only be run when building the docker image with the docker build command.

Builder base image

FROM node:18-alpine as builder

Here we are using node:18-alpine as the base image for the builder stage of our image build. node:18-alpine refers to the latest version of Node 18 which is expected to be supported until mid-2025. This is also the alpine variant of the image which uses the lightweight (5MB) Alpine Linux image that saves disk space when compared to a standard Linux image.

Copy app source files

WORKDIR /home/node/app
COPY . .

With these instructions, we set up a working directory inside the node image and copy the react app source files to it. Since we will install the npm packages and create a build from scratch in the node image, we don't need to copy the node_modules and the build output folder.

If you used create-react-app, the default build output folder is build. If you used create-vite with the npm create vite command, the default build output folder is dist. This example treats the dist folder as the build output folder, adjust it accordingly based on your project setup.

node_modules
dist

Create production build

RUN npm ci
RUN npm run build

Now we run the npm ci command in the node image to install the packages with the exact version specified in the package-lock.json file. Next, we run npm run build to create the production build.

Server stage

This stage contains the image and the instructions that will run every time the final image is run with docker run.

Server base image

FROM nginx:1.24-alpine as server

nginx:1.24-alpine refers to the 1.24 stable version of the popular nginx web server which is expected to be supported until early 2024. This is also the alpine variant of the image.

Copy build artifacts

COPY --from=builder /home/node/app/dist /usr/share/nginx/html

With this copy instruction, we copy the files or the "build artifacts" that were generated as part of the production build in the previous builder stage to the folder that will be served by nginx. If your project's build output folder is not dist, change the dist part of the /home/node/app/dist path to the folder that you use.

Copy custom config

COPY nginx.conf /etc/nginx/conf.d/default.conf

The default nginx configuration in the nginx image does not work with client-side routing solutions like React router. To overcome this, we provide the following custom configuration to overwrite the default configuration.

server {
listen 80;
listen [::]:80;

location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}

The key part of this configuration is the try_files directive which asks nginx to serve index.html at all non-existent paths, such as the client-side-only paths used by React router.

Start nginx

In this Dockerfile, we rely on the default CMD instruction in the nginx image for it to start itself.

CMD ["nginx", "-g", "daemon off;"]

The -g daemon off option asks nginx to stay in the foreground so that it can be tracked properly by docker. Without this, the container will stop immediately after starting since nginx will start in the background and docker will assume that the command has finished running.

By default, nginx will start on port 80 but it will not be accessible from the host machine. To access the container from the host machine, a custom port mapping must be provided through the -p flag when starting the container.

$ docker run -p 8000:80 react-app

In this case, the number specified on the left (8000) is the port on the host machine to which port 80 in the container will be mapped to.

Prabashwara Seneviratne

Written by

Prabashwara Seneviratne

Author. Lead frontend developer.