Docker, it’s not rocket science, Build a Minified React App and Serve on Nginx — Part II

Waleola Akinsanmi
10 min readApr 12, 2020

--

In part 1 of this series, we learned about Docker Image and Container, in this series we will dive into how to create our own Docker image and how we can create an application and expose it through a port. We will primarily be looking at Dockerfile and how to specify commands in the Dockerfile.

In the first series, the first thing we did was to type ‘docker run hello-world’ and that printed “hello world” with some other information. What is in the hello-world image? If you are curious, please visit here, to find out how it was created.

The Dockerfile of hello-world is like this

FROM scratch
COPY hello /
CMD ["/hello"]

We can see here that they are basically creating a Docker image from scratch, you must always have ‘FROM’ at the beginning of your Dockerfile. In our case, we will not start from scratch because we have awesome people who have created Docker image for almost everything we need in Docker hub and we can start building our own image ‘FROM’ the image they have created.

The second line “COPY” a binary file ‘hello’ from the same directory as the Dockerfile and copies it in the root folder of the container.

The third line CMD executes the binary file and it prints “hello world” and other information on the terminal.

Ok, Let’s build our Own Hello world Image

To build our first image, we need to create a Dockerfile.

I am currently in the docker-not-rocket-science folder, please ignore the ‘01-building-docker-image’, it is the git branch I am currently on, the README.md is also irrelevant.

The first thing is to create a Dockerfile:

touch Dockerfile

and add the code below in the Dockerfile.

FROM ubuntuCMD ["/bin/echo", "Hello world"]

To build our image we run this in the terminal

docker build .

To view the image generated

docker images

The Docker image build was successful and we can see the image id generated in the terminal correlates with the one in the ‘docker images’, Note that we don’t have a tag and it doesn’t say the repository here. To run our new docker image:

docker run --rm 860085349447

We see that “Hello world” is printed out in the console.

Running bash by default in Docker Image

we can get our docker image to drop us in ubuntu bash by updating our Dockerfile to:

FROM ubuntuCMD ["/bin/bash"]

and then we can build a new image by running

docker build --tag my-docker-image .

Now, we can see the new image is created with the tag we specified. To verify

docker images

To interact with the new image created

docker run -it --rm my-docker-image

Ok Wale, I already know how to interact with images and containers from part 1 of the write-up, I wanna do something fun hun!

Ok let’s figure out how we can bind our code in our system to a container, hahaha!!

Let’s create a src folder in our directory

mkdir src
echo "I am file a" > src/a.txt
echo "I am file b" > src/b.txt
ls src

We have created this src folder where our code lives, now we want to bind the code to run in our ubuntu container.

docker run -it --rm --mount type=bind,source="$(pwd)"/src,target=/src my-docker-image

The $(pwd) sub-command expands to the current working directory on Linux or macOS hosts.

Note that we now have a ‘src’ folder in the root of our ubuntu machine, what we have done is to bind the ‘src’ folder in our project to the root directory of our container.

Now, to confirm that our src folder in our computer is bounded to the container, we can tail the text file in the container and then make changes to it in our host machine

In the container, check for changes in the src/a.txt

tail -f src/a.txt

Now, in my machine, if I change the text in src/a.txt, it will immediately sync with the docker container.

The source code for this can be found here, to also check the difference between what we had in master and the first part, visit here.

Let’s switch gear a bit and run a simple Nodejs application in Docker.

We will set up a simple Nodejs app, build it and run it in a Docker container. You can view the starting code here.

“npm init -y”npm install express

This is the folder structure of the app I have bootstrapped. You can view the hash commit on Github.

Let’s talk about what we have in the folder

  • “src/index.js”: This is where I am spinning up the express server. Please note that the express server will run at port 8080, and the host is “localhost” which we have denoted as “0.0.0.0” here.
  • “.dockerignore”:
  • “.gitignore”: The .dockerignore and .gitignore have the same content in this case. We are asking git and docker to ignore the files because we don’t want to push our node_modules to our repository and we definitely don’t want the risk of copying the node_modules in our host machine into the docker container node_modules.
node_modulesnpm-debug.log
  • package.json: This has the information about our app and dependencies.
  • Dockerfile: The content of our Dockerfile has changed from ubuntu to node:10
FROM node:10

We are starting our application with the official Nodejs image in Dockerhub. This image will already come preinstalled with Node and Npm. All we have to worry about is to install our app’s dependencies.

FROM node:10COPY package*.json ./

The next stage is that, we are copying the package.json and package-lock.json from our host into the Docker image we are building.

FROM node:10COPY package*.json ./RUN npm install

The next thing we want to do is to install our dependencies on the Docker container. If we are running a production code, we should use `RUN npm ci — only=production`.

FROM node:10COPY package*.json ./RUN npm installCOPY src src

Now we are basically copying the src folder into the src folder of the machine. Usually, you will see “COPY . .” which means copy what is in the host machine work directory to the root of the docker container. Note that we already copied the package files, the only thing we need is to copy the src folder. You would ask why I added a “.dockerignore” if I am doing it manually, it’s just for learning purposes.

If you build your docker image at this point and run the container passing ‘bash’ as the CMD from the terminal, you will see that we have the package.json, package-lock.json file and our src folder.

FROM node:10COPY package*.json ./RUN npm installCOPY src srcCMD [ "node", "src/index.js" ]

The next thing we want to do is to fire up the express server in src/index.js. Now it is time to build our image and run the container.

docker build --tag my-first-node-app .

To run the image

docker run -it --rm --publish 3000:8080 my-first-node-app

Now, we saw a new command line argument “ — publish”, note that we are running our Express server on port 8080, “ — publish” allows us to expose the Port in our container so we can access it on our host computer on port 3000. Now, we can visit “http://localhost:3000/” to view our Nodejs app. Please visit here to view the code changes

Note: “cntr+c” will not stop the running express server, to stop it, we will need to check the docker process and kill the process. For us to be able to kill the express server with “cntr+c”, pass init flag.

docker run -it --init  --rm --publish 3000:8080 my-first-node-app

According to Docker, “You can use the — init flag to indicate that an init process should be used as the PID 1 in the container. Specifying an init process ensures the usual responsibilities of an init system, such as reaping zombie processes, are performed inside the created container”.

Understanding Multi-Stage Build by Building a Minified React app

We will use the multi-stage build approach to bootstrap our production ready minified application. According to Docker, With multi-stage builds, you use multiple FROM statements in your Dockerfile. Each FROM instruction can use a different base, and each of them begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don’t want in the final image.

The first thing we have to do is to bootstrap a React app, I will use the create-react-app.

According to react documentation, Create React App is a comfortable environment for learning React, and is the best way to start building a new single-page application in React. It sets up your development environment so that you can use the latest JavaScript features, provides a nice developer experience, and optimizes your app for production. You’ll need to have Node >= 8.10 and npm >= 5.6 on your machine. To set up your dev environmet, checkout out github and run:

npx create-react-app my-app
cd my-app
npm start

In my case, I already have a working-directory initialized with git, I had to remove all the code in my directory and then create react app in my current directory. You can check the hash commit here.

npx create-react-app .
npm start

NOTE: In order for us to understand how multi-stage build works, I have decided to use three stages:

  1. First stage: I will use node:12-stretch image to build a minified Nodejs app
  2. Second stage: I will create another image from Ubuntu and copy the already minified code output from “first stage” into the container in the second stage
  3. Third stage: I will create another image from Nginx container and we will copy the minified code in the folder to allow Nginx to run our app.

You will notice from the step above that the second stage is redundant, I already have a minified code from the first stage and I can copy the minified code directly into the Nginx container. The reason I added the second stage is to allow us to dive a little into WORKDIR and also for us to know that container is nothing but some bunch of files in a folder as explained in part 1 of the Docker series.

FROM node:12-stretch AS FIRST_STAGEWORKDIR /code/COPY package*.json /code/RUN npm ci --only=productionCOPY . /code/RUN npm run build

NOTE: We name this stage “FIRST_STAGE” (you can call it whatever you want), we then copy our package.json, package-lock.json, install our dependency and then we build the minified version of our react app.

We have used WORKDIR to specify where we want docker to use as the directory.

At this point, we will try to build the app and then try to ensure the minified code is in the build folder.

docker build --tag my-minified-react-app .docker run -it --init --rm my-minified-react-app bash

Now we can confirm that the minified code is in build “ls /code/build/”

Second Stage

Here, we will copy the minified code from the first stage build into an image we will build from Ubuntu.

FROM node:12-stretch AS FIRST_STAGEWORKDIR /code/COPY package*.json /code/RUN npm ci --only=productionCOPY . /code/RUN npm run build# stage twoFROM ubuntu:18.04 AS SECOND_STAGECOPY --from=FIRST_STAGE /code/build /minified/

NOTE: I have used Ubuntu in the second stage here because I want us to be able to run bash and ensure that our minified code has been copied from the first stage /code/build to our minified folder in stage two. In production, you want to use an alpine container.

docker build --tag my-minified-react-app .docker run -it --rm my-minified-react-app bash

Now we can confirm that the minified code is in our second stage “ls /minified/”

Third Stage

Now we want to copy the minified code from our second stage into the Nginx server so that Nginx can serve the app.

FROM node:12-stretch AS FIRST_STAGEWORKDIR /code/COPY package*.json /code/RUN npm ci --only=productionCOPY . /code/RUN npm run build# stage twoFROM ubuntu:18.04 AS SECOND_STAGECOPY --from=FIRST_STAGE /code/build /minified/#stage threeFROM nginx AS my_nginxCOPY --from=SECOND_STAGE /minified/ /usr/share/nginx/html

To test this

docker build --tag my-minified-react-app .docker run -it --rm --publish 3000:80 my-minified-react-app# Now visit http://localhost:3000/

Conclusion

It’s safe to say that Docker is just a bunch of files that have been jailed in a folder as described in the first part of this write-up. Please note that, in order to make this simple, we execute all of the commands as the root user, this is to avoid changing ownership of folders and all the complexity that comes with it. The next part of the write up will dive into how to manage users in Docker container, how to network containers, volumes and we will dive into Docker-Compose by creating a simple CRUD application with Nodejs, Mongo and React all running in containers.

Thanks for reading.

--

--

Waleola Akinsanmi

Apprentice Developer — Learning to declare variable right