Django-React App — Part2 React Development Environment

Roman Kosanovic
ascaliaio
Published in
6 min readJan 3, 2023

TL;DR: Using docker and docker-compose create an easy to use and excellent approximation of a production environment. In this example, Node’s Webpack has “hot reload” capabilities making it possible to develop a React JS application and reflect the changes immediately. Because everything is done within docker containers, there is no fear of having OS specific packages and having the app behave differently when deployed into a production environment. The principles described here can easily be applied to other frameworks that have the “hot reload” feature, such as Flask, FastAPI and many other

Introduction & Motivation

The following article is a sequel to Part 1 which describes how to set up a local development environment using docker for the backend part of an application written in Django. The principles and tools described in the first part can be applied here with a few node-specific tweaks.

React

ReactJS is a UI library for building intelligent complex UIs. Webpack is a static module bundler that goes through a package and creates a dependency graph that consists of various modules required by the app to function as expected. Depending on this graph, Webpack creates a new package, consisting of the bare minimum number of required files, that can be inserted into the html file easily. Another great thing about it is its mentioned hot reload feature that will make the browser auto-reload a page any time a change in the code is detected. This feature is part of its webpack-dev-server.

To build a development environment using docker, first a Dockerfile has to be written:

##########################################################################
# Base Stage #
# This gets our dependencies installed and out of the way #
##########################################################################
# syntax = docker/dockerfile:1.4
ARG NODE_VERSION=16.11

FROM node:${NODE_VERSION}-slim as base


# need to put here all the environment variables
ENV NODE_ENV=production \
BABEL_ENV=production

RUN --mount=type=cache,target=/var/cache/apt,id=apt \
apt-get update && apt-get upgrade -y && apt-get install -y git build-essential --no-install-recommends \
<other needed OS packages for building node modules>
&& apt-get clean \
&& (rm -f /var/cache/apt/archives/*.deb \
/var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin /var/lib/apt/lists/* || true)

WORKDIR /app

#RUN npm install npm@7.18.1 \
# && rm -rf /usr/local/lib/node_modules/npm \
# && mv node_modules/npm /usr/local/lib/node_modules/npm

COPY package*.json ./

#RUN --mount=type=cache,mode=0777,target=/root/.npm/_cacache npm config list \
# && npm install --only=production --no-optional

RUN --mount=type=cache,mode=0777,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn \
yarn install --production --non-interactive --ignore-optional

##########################################################################
# Local Development Stage #
# don't COPY in this stage because for dev you'll bind-mount anyway #
##########################################################################
FROM base as local

ARG REACT_APP_API_URL=http://localhost:8000

# need to put here all the environment variables
ENV NODE_ENV=development \
BABEL_ENV=development \
PATH=/app/node_modules/.bin:$PATH \
REACT_APP_API_URL=${REACT_APP_API_URL}


#RUN --mount=type=cache,mode=0777,target=/root/.npm/_cacache npm config list \
# && npm install --development

RUN --mount=type=cache,mode=0777,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn \
yarn install --development --non-interactive --ignore-optional

CMD [ "node", "scripts/start.js" ]

##########################################################################
# Prod intermediate Stage #
##########################################################################
FROM base as prod-intermediate

... (will be shown in a future article)

RUN yarn build

##########################################################################
# Production Stage #
##########################################################################
FROM nginx AS prod

... (will be shown in a future article)

CMD [ "nginx", "-g", "daemon off;" ]

As in Part 1, the focus will be on the development stages of the Dockerfile that will create our development environment.

A few things to note:

  • Base Stage — we start from a debian slim node image whose version we can change in build time. This stage builds first all the production deps which are also common to the dev stage
  • APT packages are again persisted across builds using the fantastic Docker BuildKit Engine:
    RUN --mount=type=cache,target=/var/cache/apt,id=apt \\
  • our frontend team prefers yarn over npm, however an example of how to persist packages for npm is shown in the comments:
    RUN --mount=type=cache,mode=0777,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn \\
    OR for npm:
    RUN --mount=type=cache,mode=0777,target=/root/.npm/_cacache npm config list \\
  • Webpack’s dev server is started via a script in this case but it can also be started using yarn: yarn start
  • Pointing the frontend to a backend api is configurable via this environment variable REACT_APP_API_URL=${REACT_APP_API_URL} and the default value is http://localhost:8000 where the django backend container from Part 1 is listening on. There is also a way to have configurable environment variables in production runtime but that’s another topic. For more info read this article.
  • As in the Django example, the code isn’t copied into the container, rather it’s mounted as specified in the simple docker-compose.ymlfile. The frontend dev container will be listening on port 3000.
version: '2.5'

services:
react-frontend:
image: react-frontend
build:
context: .
target: local
ports:
- "3000:3000"
volumes:
- frontend-modules:/app/node_modules
- .:/app:delegated
environment:
- CHOKIDAR_USEPOLLING=true

volumes:
frontend-modules:

Node modules are built within the container, making sure the host OS binaries aren’t involved in the build, thereby guaranteeing the same behavior as in production.

That’s it! Having these files is enough to create and manage a local development environment that emulates a production one, without having to install NodeJS or node modules on the local machine.

To manage the environment, these commands can be used:

# Build the local development image, don't specify the first two build arguments
# if you want to use the defaults
docker-compose build --build-arg NODE_VERSION=<some node version> --build-arg REACT_APP_API_URL=<some location> --build-arg BUILDKIT_INLINE_CACHE=1 --progress=plain

# Run the dev container
docker-compose up -d

# Stop the container when finished
docker-compose down

# Cleanup, remove the docker volume containing node modules
docker volume rm <repo_name>_frontend-modules || true

If we want even more automation, a Makefile can be used just like it was in Part 1 for the backend container. The following section is purely optional and can be skipped.

More automation — Make (Optional)

The Makefile will practically look the same as the one in the Django backend repository. The only difference are the names of build arguments in the docker-compose build command as shown above.

It again comes down to:

# Display targets
make help

# Build and start the dev container using the defaults
make local_dev_env

# Build and start the dev container using a different node version file
make -e NODE_VERSION=<some_version> -e REACT_APP_API_URL=<some_location> local_dev_env

# Stop container, clean all the untagged build layers after multiple rebuilds and remove volume
make clean_local

Tieing it all together

We now have a way to create a local development environment for both the backend and the frontend part of the app. The environment emulates the production image in all but the serving part, for which the development servers are used in order to have the hot reload feature available.

By running the respective docker-compose commands in each repository, or just make local_dev_env in both repos, a developer can spin a frontend and backend container that talk to each other in a very short time. Changing code in either repositories is immediately reflected upon saving the changes thus making it possible to easily catch bugs introduced by any changes.

Summary

As a small recap, here’s a list of some of the advantages this setup provides:

Using one or two commands in both the backend and frontend repository, run development containers for frontend and backend without having to install the languages and app dependencies on your local machine

Easily try your code with another language version just by changing a single environment variable, NODE_VERSION .

Easily point your frontend app to any backend by changing the REACT_APP_API_URL .

The containers are using the same base image and deps as the production images, making them a good approximation of a production environment. This avoids differing behavior due to different OS binaries (e.g. MAC and Ubuntu).

Immediately reflect changes in code and catch bugs.

Rebuilds are fast thanks to BuildKit caching that makes the Dockerfile a good candidate for usage within a CICD pipeline.

Future articles will show how a production image can be built for both parts of the app and how to utilize BuildKit’s awesome features in a CICD system to ensure fast and efficient builds.

Thanks for reading, hope it was clear and useful :)

--

--

Roman Kosanovic
ascaliaio

Senior DevOps/SRE Engineer and former physicist