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