Docker: React, Express & Reverse Proxy

Alex Shepherd
8 min readMar 2, 2020
Photo by Johan Taljaard on Unsplash

In this tutorial we will be building and running three Docker containers, all running in one docker network.

  • API (Express)
  • Client (React)
  • and a reverse proxy (Nginx)

We’re using the reverse proxy here so that the communication between our frontend and backend is running on one origin. This means we don’t need to enable CORS, but there are many other advantages to running a reverse proxy too.

You can read more about reverse proxies on the Nginx website, or about CORS at MDN.

We’ll be using the following repos:

React App: https://github.com/frontendfoo/docker-reactApp
Express App: https://github.com/frontendfoo/docker-expressApp
Reverse Proxy: https://github.com/frontendfoo/docker-reverseProxy

You’ll also need docker installed.

We are going to create docker image for each of the above and then run them locally.

React app container

Let’s start by creating a docker image of a React Application.

Clone down the react app. It’s a standard parcel app which you should be able to install, run and build — though this is not necessary. If you do run it, you’ll get a page with ‘error’ for now as there is no backend to respond.

$ git clone https://github.com/frontendfoo/docker-reactApp.git

Create a dockerfile in the apps root directory. This is where the build instructions for the image will be listed. You’ll notice that it is split in two — stage one and stage two — this is called a multi-stage build. What this will do is temporarily create a node container to build our application, then the bundled application will be copied into an Nginx container, which will produce the image from.

# dockerfile# stage 1
FROM node:13.12.0-alpine as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY . /app
RUN npm ci --loglevel verbose
RUN npm run build
# # stage 2
FROM nginx:1.16.0-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Each line of a dockerfile is a step.

Stage One:

  1. Spin up a node container
  2. Change the working directory to /app
  3. Add node_modules to PATH
  4. Copy everything locally to the containers /app directory
  5. Install python (dependencies for node-gyp)
  6. Install JS dependencies. See npm ci
  7. Run the build script

Stage Two:

  1. Spin up an Nginx container
  2. Copy from build the bundled app to the Nginx public directory.
  3. Expose port 80 (for HTTP requests)
  4. Provide default for execution: $ nginx -g daemon off

Stage two, step six will execute when a container is created from the image. By default Nginx will run in the background as a daemon, this would mean that when we create a container from our image it would immediately shutdown. In order to keep the container running we need to run Nginx in the foreground. See CMD in the docker documentation for more info.

Next we will create a dockerignore file. This will prevent unneeded files from being available in dockers context. When we copy files into the first stage of the build they will be ignored.

Create a .dockerignore file in the apps root directory.

# .dockerignorenode_modules
dist
.cache

We are now ready to build our image. From the apps root directory run:

$ docker build -t reactapp:1.0.0 .

This will build the image reactapp and tag -t it with version 1.0.0. The period . on the end is important, and should be the relative location to the dockerfile.

Now if we list images we should see our newly created reactapp image.

$ docker image ls

Express app container

Next we’ll create an image of the API.

Clone down the express app

$ git clone https://github.com/frontendfoo/docker-expressApp.git

Again you can install the dependencies and run this app locally if you like.

Take note: This npm package uses ES6 modules so you will need to run it in a node 13.2.0 environment or newer. If you do spin this package up locally, the frontend will be able to fulfil its request.

Let’s create a dockerfile, though this will be slightly different from last time.

# dockerfileFROM node:13.12.0-alpine
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY . /app
RUN npm ci --loglevel verbose
EXPOSE 80
CMD [ "node", "index.js" ]

This dockerfile only has one stage.

  1. Spins up a node container
  2. Switches working directory to /app
  3. Adds node_modules to PATH
  4. Copies local files to /app
  5. Installs dependencies
  6. Exposes port 80 (for HTTP requests)
  7. Provide default for execution: $ npm run start

We’ll also need a dockerignore file in the apps root directory.

# .dockerignorenode_modules

We can now build the backend image.

$ docker build -t expressapp:1.0.0 .

Again we can list the images and see the newly created expressapp image.

$ docker image ls

Reverse proxy container

For our last container we’ll use Nginx as a reverse proxy.

Clone down the Nginx config

$ git clone https://github.com/frontendfoo/docker-reverseProxy.git

The repo contains a config for setting up a reverse proxy in Nginx. The most important part of this file is the server block.

server {
listen 80;
location /api {
proxy_pass http://backend/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
location / {
proxy_pass http://frontend;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}

All requests to /api will be forwarded proxied to http://backend/ (notice the trailing forward slash, this is important) and / to http://frontend .

The host names backend and frontend will relate to the names that we give our containers when we spin them up later.

Let’s create the dockerfile for this image.

# dockerfileFROM nginx:1.16.0-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Again, one stage:

  1. Spin up an Nginx container
  2. Remove the existing config
  3. Copy in the new config
  4. Expose port 80
  5. Provide default execution $ nginx -g daemon off

We don’t need a dockerignore file for this image so let’s go straight to the build.

$ docker build -t reverseproxy:1.0.0 .

One final list of our images

$ docker image ls

Spin up a multi-container Docker application

Now we have all our images created, we are going to want to run them. Firstly we will need a network for them to all run in.

Create a network

$ docker network create sub-etha

Here we create a network called sub-etha. You can call your network whatever you like as long as you use it consistently.

We can checkout our network to see what containers are attached. There wont be any for now.

$ docker network inspect sub-etha

Now let’s spin up all our containers and attach them to the network

Let’s start with the frontend and backend applications.

$ docker run -d --name=backend --network=sub-etha expressapp:1.0.0
$ docker run -d --name=frontend --network=sub-etha reactapp:1.0.0

The two commands above will

  1. -d run the container in detached mode
  2. --name give the containers names (same as we used in the reverse proxy)
  3. --network attach the containers to the network
  4. express:1.0.0 the image we want to spin up

Next lets spin up the reverse proxy

$ docker run -d --name=revproxy --network=sub-etha -p 80:80 reverseproxy:1.0.0

The additional option here is -p 80:80 this maps the containers port 80 to the host machines port 80.

We can now list current running containers

$ docker container ls

You should see all three containers listed

We can also inspect the network

$ docker network inspect sub-etha

Great, so now we can go ahead and open up http://localhost in a browser.

Docker Compose

We have our multi-container Docker application up and running, but what if we could build and spin this up with one command. Well thats where Docker Compose comes in. We can write all the configuration in a YAML file then with one command build and run all the services.

You will need Docker Compose installed, though you may already have it. Checkout the Docker Compose installation documentation.

Let’s start by removing all three containers and network.

$ docker stop frontend backend revproxy
$ docker container rm frontend backend revproxy
$ docker network rm sub-etha

Create a new directory called docker-compose along side the other repos.

Inside the new directory create a docker-compose file

# ./docker-compose/docker-compose.ymlversion: '3'
services:
frontend:
build: ../docker-reactApp
image: "reactapp:1.0.0"
networks:
- sub-etha
backend:
build: ../docker-expressApp
image: "expressapp:1.0.0"
networks:
- sub-etha
revproxy:
build: ../docker-reverseProxy
image: "reverseproxy:1.0.0"
networks:
- sub-etha
ports:
- "80:80"
networks:
sub-etha:

So whats happening here?

  1. Firstly we declare the version of docker-compose
  2. We then declare the services
  3. Each service we can give a name
  4. build is used to point to were the app and dockerfile is, to build the images
  5. image docker compose will name and tag the image
  6. networks declares which networks to add the container too
  7. ports declares which ports to map
  8. Finally we must create the network sub-etha

All we have left to do now is run it

$ docker-compose up -d

This will build each image, spin them up and add them to the network.
The-d option will run the containers in detached mode.

Open up http://localhost in a browser to admire your hard work.

To stop the containers

$ docker-compose stop

Additional Docker commands

Here’s some commands that you may find useful.

Containers

# list running containers
$ docker container ls
# list running and stoped containers
$ docker container ls -a
# stop a container
$ docker stop frontend
# start a container
$ docker start frontend
# Remove a container
$ docker container rm frontend
# Remove all stopped containers
$ docker container prune

Images

# list images
$ docker image ls
# delete an image
$ docker image rm reactapp:1.0.0
# delete dangling images
$ docker rmi $(docker images -f "dangling=true" -q)

Networks

# list networks
$ docker network ls
# create a network
$ docker network create sub-etha
# delete a network
$ docker network rm sub-etha
# remove unused networks
$ docker network prune
# inspect a network
$ docker network inspect sub-etha

Docker Compose

# start containers in detached mode (-d)
$ docker-compose up -d
# stop running containers
$ docker-compose stop
# stop containers and remove containers, networks and images
$ docker-compose down

Execute

$ docker exec -it <CONTAINER_ID> /bin/sh

--

--