Building and deploying React application in Docker
This post describes how to build and deploy a React application in a Docker container. The post assumes that you have a React application that you want to deploy and that you have a Docker environment set up. After following the steps in this post, you should have a React application that is built in a Docker container and deployed to a server.
Dockerfile#
The application is written in pure React, without any server components. That means, it builds into a set of static files and can be served with a web server. Initially, I tried to use Nginx, but it didn't go well. I didn't want to spend more time and switched to the serve
package. The Dockerfile looks like this:
####################################################################################################
## Builder
####################################################################################################
FROM node:22 as builder
# Set up a working directory
WORKDIR /usr/src/myapp
# Copy the package.json and package-lock.json and install dependencies
COPY package.json package-lock.json ./
RUN npm install
# Accept environment variable
ARG REACT_APP_CLERK_PUBLISHABLE_KEY
# Copy the source code into the Docker image
COPY . .
RUN REACT_APP_CLERK_PUBLISHABLE_KEY=${REACT_APP_CLERK_PUBLISHABLE_KEY} npm run build
####################################################################################################
## Final image
####################################################################################################
FROM alpine:latest
# Install Node.js and npm
RUN apk add --update nodejs npm
# Install serve globally
RUN npm install -g serve
COPY --from=builder /usr/src/myapp/build /myapp
WORKDIR /myapp
# Start the server
ENTRYPOINT ["serve", "-s", "."]
# Expose the port serve runs on
EXPOSE 3000
The serve
thing might be a performance issue in production environment, but it's fine for development. This line RUN REACT_APP_CLERK_PUBLISHABLE_KEY=${REACT_APP_CLERK_PUBLISHABLE_KEY} npm run build
helps to pass an environment variable to the build process - since it is a React application, the environment variables should be built-in during the build process.
I tried to use node:alpine
for the image, but it takes more than 1GB of space. So, here I use node:22
which takes only around 60 MB.
Github Workflow#
The Github Workflow is used to automate the build and push process. The workflow looks like this:
#
name: Build and Push Docker Dev Image
on:
push:
branches:
- main
env:
IMAGE_NAME: myappbin-client
jobs:
push:
runs-on: self-hosted
permissions:
packages: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build Docker Image
run: docker build . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" --build-arg REACT_APP_CLERK_PUBLISHABLE_KEY=${{ secrets.DEV_CLERK_PUBLISHABLE_KEY }}
- name: Log in to registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
# This changes all uppercase characters to lowercase.
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# This strips the git ref prefix from the version.
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# This strips the "v" prefix from the tag name.
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# This uses the Docker `latest` tag convention.
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION
- name: Trigger Portainer Webhook
run: curl -X POST ${{ secrets.DEV_PORTAINER_WEBHOOK_URL }}
This line --build-arg REACT_APP_CLERK_PUBLISHABLE_KEY=${{ secrets.DEV_CLERK_PUBLISHABLE_KEY }}
helps to pass an environment variable to the build process.
Regarding the self-hosted
runner and Trigger Portainer Webhook
, please refer to my another post about deploying Rust on arm64 for more details.
Configuring nginx#
The Nginx configuration is absolutely standard. Assuming that the container is listening on port 8001, the proxy-part looks like this:
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
Conclusion#
A React application can easily be deployed in a Docker container. The serve
package is a good choice for serving static files. The Github Workflow is a good way to automate the build and push process. The Nginx reverse-proxy configuration is standard and easy to set up.