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
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" ]
Add a .dockerignore
, this will help us ignore node_modules
, .env
etc
**/node_modules
**/npm-debug.log
build
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
Let's start our react app for development!
docker-compose -f docker-compose.dev.yml up
We can also add it to our package.json
"dev": "docker-compose -f docker-compose.dev.yml up"
we can use the -d
flag to run in daemon mode
Let's check our container!
docker ps
REPOSITORY TAG IMAGE ID CREATED SIZE
app-dev latest 5064f3e40c97 About a minute ago 436MB
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;
}
}
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;"]
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
Build production image
docker-compose -f docker-compose.prod.yml build
Let's check out our built production image
docker images
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
let's start our production container on port 80
with the name react-app
docker run -p 80:80 --name react-app app-prod
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";
}
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.
Latest comments (23)
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
thank you!
when i run the docker compose file im getting this error
The Compose file is invalid because:
Service app has neither an image nor a build context specified. At least one must be provided.
Hi, I'm only trying to reproduce the steps for production. I'm getting:
yarn run v1.22.17
error Command "build" not found.
Hi, article easy to reproduce, thanks! But for real I don't understand why I need containers? Only for different versions of node and packages?
Thanks! yes, we don't have to use them but I find them to be useful for the following:
...and many more!
when i am running the dev image it's building it correctly but when i run the production one i got this error :
i checked the code and it's exactly the same as yours!
Hi, thank you very much for this article.
There's a lot of articles about that, but not for the build.
Glad it was helpful!
Great article, but unfortunately not a word about using
process.env
variables neither here nor in "dockerizing node app" articleIn 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.
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...
that's amazing, thanks for sharing!
Thanks! This just saved me on M1 (ran into an issue with Node dependencies not installing properly).
I’m glad it helped!