Securing React apps with React + NGINX non-root containers
This article demonstrates how to secure your React front-end applications by serving it from a non-root NGINX container.
NGINX is an open-source web server which is very popularly used to serve web applications. It can be installed on the machine locally or as a container using Docker.
Creating a React + NGINX container
Here is a Dockerfile that can be used to containerize a React application and serve it using a NGINX container:
# Use a specific version of node as the base image
FROM node:20.12.0-alpine as build
# Set the working directory to /app
WORKDIR /app
# Copy package.json to the working directory
COPY package.json /app/package.json
# Install npm packages quietly
RUN npm install --quiet
# Copy the rest of the application to the working directory
COPY . /app
# Build the application
RUN npm run build
# Use the nginx image version 1.25 as the base image
FROM nginx:1.25-alpine
# Copy the nginx configuration file to the appropriate location
COPY nginx.conf /etc/nginx/nginx.conf
# Copy the built application from the first build stage to the nginx directory
COPY --from=build /app/build /var/www
# Expose port 80 for the application
EXPOSE 80
To create the docker image from this Dockerfile, use the command:
docker build -t my-app -f Dockerfile .
To run the docker container based on the above created docker image, use the command:
docker run -d --name my-app -p 7241:80 my-app
In the above command, we are exposing the app on port 80 which gets mapped to port 7241 on our local machine.
Now try to access the application using http://localhost:7241 and you would see the page below:
Now that the application is running successfully and can be accessed, let's try to understand the app is running in the container as which user.
Run the following command on the running container:
docker exec -it my-app /bin/sh
The above command should open a shell. In the shell, run the following command (as shown in screenshot):
whoami
As you can see, the application is running as root container. Now, let's try to understand why this is a security risk.
Root vs Non root container
If you run your app as root, your app process can do anything in the container, like modify files, install packages, or run arbitrary executables. That’s a concern if your app is ever attacked. Whereas hosting containers as non-root aligns with principle of least privilege. It’s free security provided by the operating system. If you run your app as non-root, your app process cannot do much, greatly limiting what a bad actor could accomplish.
Securing the app using non root React + NGINX container
Now that you have understood the difference between root and non-root containers, let's try to modify the above Dockerfile to switch to a non-root container.
There is not much change to do. The only major change is switching from nginx to nginxinc/nginx-unprivileged in the Dockerfile.
Here is a Dockerfile that can be used to containerize a React app and serve it using a NGINX container:
# Use a specific version of node as the base image
FROM node:20.12.0-alpine as build
# Set the working directory to /app
WORKDIR /app
# Copy package.json to the working directory
COPY package.json /app/package.json
# Install npm packages quietly
RUN npm install --quiet
# Copy the rest of the application to the working directory
COPY . /app
# Build the application
RUN npm run build
# Use the nginxinc/nginx-unprivileged image version 1.25-alpine as the base image for the second build stage
FROM nginxinc/nginx-unprivileged:1.25-alpine
# Copy the nginx configuration file to the appropriate location
COPY nginx.conf /etc/nginx/nginx.conf
# Copy the built application from the first build stage to the nginx directory
COPY --from=build /app/build /var/www
# Expose port 8080 for the application
EXPOSE 8080
In addition to the above, please ensure that you have the following line in your nginx.conf.
pid /tmp/nginx.pid;
This is a well-known configuration requirement for nginxinc/nginx-unprivileged image.
Also, ensure that nginx is listening on port 8080 as port 80 cannot be used by non-root user as port 80 is a privileged port. Check for the following:
listen 8080;
To create the docker image from this Dockerfile, use the command:
docker build -t my-app-non-root -f Dockerfile .
To run the docker container based on the above created docker image, use the command:
docker run -d --name my-app-non-root -p 7242:8080 my-app-non-root
In the above command, we are exposing the app on port 8080 which gets mapped to port 7242 on our local machine.
Now try to access the application using http://localhost:7242 and you would see the page below:
Now that the application is running successfully and can be accessed, let's try to understand the app is running in the container as which user.
Run the following command on the running container:
docker exec -it my-app-non-root /bin/sh
The above command should open a shell. In the shell, run the following command (as shown in screenshot):
whoami
As you can see, the application is running as non-root (nginx user) container and we have added a layer of security.