DEV Community

Cover image for Dockerize your React app
Karan Pratap Singh
Karan Pratap Singh

Posted on • Updated on

Dockerize your React app

Hey, welcome back. This article is part of the Dockerize series, make sure to checkout the Introduction where I go over some concepts we are going to use.

Today we'll dockerize our React application by taking advantage of builder pattern with multi stage builds for optimization!

I've also made a video, if you'd like to follow along

Project setup

I've initialized a pretty standard react project using the default create react app (CRA) template.

All the code from this article will be available in this repo

├── node_modules
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.js
│   ├── index.css
│   ├── index.js
│   └── logo.svg
├── package.json
└── yarn.lock
Enter fullscreen mode Exit fullscreen mode

For development

Let's start by adding a Dockerfile

FROM node:14-alpine AS development
ENV NODE_ENV development
# Add a work directory
WORKDIR /app
# Cache and Install dependencies
COPY package.json .
COPY yarn.lock .
RUN yarn install
# Copy app files
COPY . .
# Expose port
EXPOSE 3000
# Start the app
CMD [ "yarn", "start" ]
Enter fullscreen mode Exit fullscreen mode

Add a .dockerignore, this will help us ignore node_modules, .env etc

**/node_modules
**/npm-debug.log
build
Enter fullscreen mode Exit fullscreen mode

Let's create a docker-compose.dev.yml. Here we'll also mount our code in a volume so that we can sync our changes with the container while developing.

version: "3.8"

services:
  app:
    container_name: app-dev
    image: app-dev
    build:
      context: .
      target: development
    volumes:
      - ./src:/app/src
    ports:
      - 3000:3000

Enter fullscreen mode Exit fullscreen mode

Let's start our react app for development!

docker-compose -f docker-compose.dev.yml up
Enter fullscreen mode Exit fullscreen mode

We can also add it to our package.json

"dev": "docker-compose -f docker-compose.dev.yml up"
Enter fullscreen mode Exit fullscreen mode

we can use the -d flag to run in daemon mode

Let's check our container!

docker ps
Enter fullscreen mode Exit fullscreen mode
REPOSITORY          TAG                   IMAGE ID       CREATED              SIZE
app-dev            latest                5064f3e40c97   About a minute ago    436MB
Enter fullscreen mode Exit fullscreen mode

Over 400mb!! Don't worry, this is just for development. We'll optimize our production build with builder pattern!

For production

We'll use nginx to serve our static assets and will help resolve routes when we're using React Router or any kind of routing.

Note: Personally, I do not recommend using static server packages like serve in production, nginx gives us much more performance and control

Let's create a nginx.conf

server {
  listen 80;

  location / {
    root /usr/share/nginx/html/;
    include /etc/nginx/mime.types;
    try_files $uri $uri/ /index.html;
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's update our Dockerfile for production

FROM node:14-alpine AS builder
ENV NODE_ENV production
# Add a work directory
WORKDIR /app
# Cache and Install dependencies
COPY package.json .
COPY yarn.lock .
RUN yarn install --production
# Copy app files
COPY . .
# Build the app
RUN yarn build

# Bundle static assets with nginx
FROM nginx:1.21.0-alpine as production
ENV NODE_ENV production
# Copy built assets from builder
COPY --from=builder /app/build /usr/share/nginx/html
# Add your nginx.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]
Enter fullscreen mode Exit fullscreen mode

Let's add a docker-compose.prod.yml file

version: "3.8"

services:
  app:
    container_name: app-prod
    image: app-prod
    build:
      context: .
      target: production
Enter fullscreen mode Exit fullscreen mode

Build production image

docker-compose -f docker-compose.prod.yml build
Enter fullscreen mode Exit fullscreen mode

Let's check out our built production image

docker images
Enter fullscreen mode Exit fullscreen mode

Using builder pattern we reduced out image size to just ~23mb!!

REPOSITORY          TAG                   IMAGE ID       CREATED              SIZE
app-prod           latest                c5db8d308bb9   About a minute ago   23.1MB
Enter fullscreen mode Exit fullscreen mode

let's start our production container on port 80 with the name react-app

docker run -p 80:80 --name react-app app-prod
Enter fullscreen mode Exit fullscreen mode

Optimizing static assets (Bonus)

You can also add the following inside the location block to introduce caching for our static assets and javascript bundle.

You can refer this guide to dive deep into optimizing

# Cache static assets
location ~* \.(?:jpg|jpeg|gif|png|ico|svg)$ {
  expires 7d;
  add_header Cache-Control "public";
}

# Cache css and js bundle
location ~* \.(?:css|js)$ {
  add_header Cache-Control "no-cache, public, must-revalidate, proxy-revalidate";
}
Enter fullscreen mode Exit fullscreen mode

Next steps

With that, we should be able to take advantage of docker in our workflow and deploy our production images faster to any platform of our choice.

Feel free to reach out to me on Twitter if you face any issues.

Top comments (22)

Collapse
 
salhernandez profile image
Salvador Hernandez

This is great! I like putting react apps in containers!

I created a docker compose files that runs create-react-app based apps loacally on Docker!

dev.to/salhernandez/containerize-b...

Collapse
 
karanpratapsingh profile image
Karan Pratap Singh

that's amazing, thanks for sharing!

Collapse
 
spyshow profile image
jihad khorfan • Edited

when i am running the dev image it's building it correctly but when i run the production one i got this error :

  [builder 7/7] RUN yarn build:
15 1.300 Usage Error: Couldn't find the node_modules state file - running an install might help (findPackageLocation)
15 1.300
15 1.300 $ yarn run [--inspect] [--inspect-brk] <scriptName> ...
executor failed running [/bin/sh -c yarn build]: exit code: 1
ERROR: Service 'app' failed to build : Build failed
Enter fullscreen mode Exit fullscreen mode

i checked the code and it's exactly the same as yours!

Collapse
 
mikebernal profile image
Mike Bernal SP0701 • Edited

The Compose file is invalid because:
Service app has neither an image nor a build context specified. At least one must be provided.

I have similar code as the tutorial tho I am using ubuntu

Collapse
 
dthuan123 profile image
dthuan123

The Compose file is invalid because:
Service app has neither an image nor a build context specified. At least one must be provided.

Collapse
 
karanpratapsingh profile image
Karan Pratap Singh

Hey, thank you for reporting this. I've updated the article to include context...are you on the latest docker-compose version?

Collapse
 
dthuan123 profile image
dthuan123
Comment
Enter fullscreen mode Exit fullscreen mode
Collapse
 
xreyc profile image
Reyco Seguma

Add to docker-compose.dev.yml for live reload
environment:
- WATCHPACK_POLLING=true

updated:

version: "3.8"

services:
app:
container_name: app-dev
image: app-dev
build:
target: development
volumes:
- ./src:/app/src
ports:
- 3000:3000
environment:
- WATCHPACK_POLLING=true

Collapse
 
ddexster profile image
DDexster

Great article, but unfortunately not a word about using process.env variables neither here nor in "dockerizing node app" article

Collapse
 
vitomanu96 profile image
vitomanu96 • Edited

In CRA all the file under /static folder can be cached without problem (they include hash in their filename). So se paragraph "Optimizing static assets" can be improved.
Just the file under the "public" folder cannot be cached by the browser.

However nginx has a optimal built-in cache strategy.

Collapse
 
moonraker727 profile image
Alex • Edited

Hi, I'm only trying to reproduce the steps for production. I'm getting:
yarn run v1.22.17
error Command "build" not found.

Collapse
 
lindsaykwardell profile image
Lindsay Wardell

Thanks! This just saved me on M1 (ran into an issue with Node dependencies not installing properly).

Collapse
 
karanpratapsingh profile image
Karan Pratap Singh

I’m glad it helped!

Collapse
 
claysam75 profile image
claysam75

After you have updated the Dockerfile for production, you have another two stages - one which actually builds the react app, and the last stage which grabs those built static files from the build step and serves them via NGINX.

Firstly, is the lower case 'as' in the nginx build step a typo or does that do something different to 'AS'.

Secondly, in the docker-compose.prod.yml file, you set the build target as 'production' - ie the last nginx stage in the docker file. How can that run successfully if the build stage it relies on has not been run yet? Is that build stage called when the nginx stage runs?

Collapse
 
karanpratapsingh profile image
Karan Pratap Singh

Hey, nice catch! yes it's a typo and should be 'AS', in dockerfile lowercase syntax also works exactly the same but it's not prefered..i'll update it to upper case..thank you

For the second question, docker automatically understands that "builder" is used inside the prod stage so it runs the builder first..so the seq of events are like this:

  1. looks in at the target, creates a dependency graph (what requires what)
  2. identifies that prod is relied on builder
  3. runs the builder first
  4. continues with the prod
Collapse
 
damiensn profile image
Damien S

Hi, thank you very much for this article.
There's a lot of articles about that, but not for the build.

Collapse
 
karanpratapsingh profile image
Karan Pratap Singh

Glad it was helpful!