Start Docker from scratch

Docker

Docker is widely used but I haven’t used it in my career. However, it is one of very useful softwares so it’s time to learn. I developed some applications running in a server but I needed to investigate what applications were running in the server and which module versions were used there in order to make sure that my application could run. If we want to update one of module versions in a server to make the new application run we have to confirm which applications use it. Huh… Don’t know until it’s done! Good bye such a work! Let’s try to dockerlize our application. We can make sure that the application works in a target computer if we develop it in a docker container because the container environment is definitely the same.

Complete source code here. Download and try to modify it.

This is one of Docker learning series posts.

  1. Start Docker from scratch
  2. Docker volume
  3. Bind host directory to Docker container for dev-env
  4. Communication with other Docker containers
  5. Run multi Docker containers with compose file
  6. Container’s dependency check and health check
  7. Override Docker compose file to have different environments
  8. Creating a cluster with Docker swarm and handling secrets
  9. Update and rollback without downtime in swarm mode
  10. Container optimization
  11. Visualizing log info with Fluentd, Elasticsearch and Kibana
Sponsored links

Create base image

I will create an application written in Typescript with Node.js, so I need to install Node.js in a container. In order to create a Docker container and run it we have to define the image by Dockerfile (without extension) and define Node.js version there. The first Dockerfile looks like following.

FROM node:14.14.0

This file defines which Node.js version to use. Great thing here is lots of modules are available in Docker-hub and Node.js is also available there! There are many options for Node.js as you can see here. The definition above is standard Node.js image which contains dev tools like npm. So we need to use it for our development. But for release, we can replace it with minimal version. If you created the Dockerfile run the following command.

cd <directory where Dockerfile exists>
docker image build -t nodejs-dev .

It starts downloading the Node.js and name the image nodejs-dev by -t nodejs-dev and dot at the end is current directory where Dockerfile exists. docker image build requires directory path and looks for Dockerfile for build. After the command, run this command to check the result.

docker image ls

My result looks like this. yuto/nodejs-dev is there because I specified -t yuto/nodejs-dev.

$ docker image ls
REPOSITORY          TAG     IMAGE ID            CREATED             SIZE
yuto/nodejs-dev     latest  63dd52d64251        4 days ago          943MB

Let’s create an image for production version too in the same way.

Dockerfile

FROM node:14.14.0-slim

Command

docker image build -t nodejs .

Now, we have base images which we want to install to our container.

Create simple application

Let’s prepare an application and try to build it in a Docker container. The application is just to display Hello World..

/src/hello-world
|--Dockerfile
|--lib
|  |--app.ts
|--package.json
|--tsconfig.json

app.ts

console.log("Hello, World.")

Dockerfile

Dockerfile looks like this.

FROM yuto/nodejs-dev as builder

RUN npm install typescript -g
WORKDIR /hello-world

COPY ./tsconfig.json .
COPY ./package.json .
COPY ./lib/ ./lib/
RUN npm run build

FROM yuto/nodejs
WORKDIR /hello-world
CMD [ "node", "./dist/app.js" ]
COPY --from=builder /hello-world/dist/ /hello-world/dist/

Even if you look Dockerfile for the first time I think you can understand what it does. The commands used here are simple.

  • FROM: Specifies docker image used in this docker image. Node.js dev version is used here to build application.
  • RUN: Literary runs the command. npm install typescript -g is npm command and it installs typescript in global space.
  • COPY: Copy file(s) from host machine to docker container.
  • CMD: This command is executed when docker container starts up.

Docker stores each command’s result and it uses the cache if no change is applied. That’s why I defined npm install typescript -g command after FROM command. We don’t want to install it every time it’s built. If the command is defined before calling npm run build it’s installed every time.

The following is the result when I modified app.ts. Cache is used on many steps because they were not updated.

$ docker image build -t hello-world .
Sending build context to Docker daemon   12.8kB
Step 1/11 : FROM yuto/nodejs-dev as builder
 ---> 63dd52d64251
Step 2/11 : RUN npm install typescript -g
 ---> Using cache
 ---> 7526a5e4fa3d
Step 3/11 : WORKDIR /hello-world
 ---> Using cache
 ---> 4d61915c2ab0
Step 4/11 : COPY ./package.json .
 ---> Using cache
 ---> 2245fc45bb67
Step 5/11 : COPY ./tsconfig.json .
 ---> Using cache
 ---> f095defb45f4
Step 6/11 : COPY ./lib/ ./lib/
 ---> 9c4d0e7d2c7e
Step 7/11 : RUN npm run build
 ---> Running in 0efdf1bce52b

> test-web@1.0.0 build /hello-world
> tsc

Removing intermediate container 0efdf1bce52b
 ---> 6cbfe806088c
Step 8/11 : FROM yuto/nodejs
 ---> c0f0d070c334
Step 9/11 : WORKDIR /hello-world
 ---> Using cache
 ---> 6b123e6d2b5a
Step 10/11 : CMD [ "node", "./dist/app.js" ]
 ---> Using cache
 ---> 7210771fe86f
Step 11/11 : COPY --from=builder /hello-world/dist/ /hello-world/dist/
 ---> Using cache
 ---> a915c79a2f94
Successfully built a915c79a2f94
Successfully tagged hello-world:latest

Run the docker container

Now we have docker image which displays Hello, world.. Let’s run the container with this command!!

$ docker container run --rm hello-world
Hello, World.

YES! It works. This is our first step.
--rm option removes the container when the container exits. If we execute the run command without it the docker container remains like this below. We can remove those unnecessary containers at once but it’s better to add --rm option.

$ docker container ls -a
CONTAINER ID        IMAGE                                           COMMAND                  CREATED             STATUS                      PORTS               NAMES
f545438f0558        hello-world                                     "docker-entrypoint.s窶ヲ"   29 seconds ago      Exited (0) 28 seconds ago                       quirky_nash
b1bfb2672c9c        hello-world                                     "docker-entrypoint.s窶ヲ"   40 seconds ago      Exited (0) 39 seconds ago                       cool_keldysh

Multi-stage build

Did you wonder why there are two FROM command in the Dockerfile?

FROM yuto/nodejs-dev as builder
...

FROM yuto/nodejs
WORKDIR /hello-world
CMD [ "node", "./dist/app.js" ]
COPY --from=builder /hello-world/dist/ /hello-world/dist/

First image is for development and second one is for production. We need Development Tools only for development process but not for production. The first image size is 943MB and second image size is 167MB. Last COPY command copies files from 1st stage to second stage which will be the actual image. By doing that, we can reduce the container size!

Dockerfile segregation

It is actually not necessary to separate the Dockerfile to create or use base image. We can directly use FROM node:14.14.0 in our application Dockerfile. But what if we want to update the Node.js version in all applications? In this case, we have to update the version number manually for each Dockerfile and maybe one of Dockerfile isn’t updated. In that case, two Node.js version runs in a container and container size becomes bigger because the container requires two Node.js versions. If we create our base image like yuto/nodejs-dev we update only one Dockerfile and it won’t happen. But in this case of course, we need to test all applications work with the updated Node.js version.

Conclusion

It was simple Docker tutorial. Let’s try to modify the code and check how docker works. If dev-environment can be established in a docker container we can try to download softwares and try them there without making a host machine messy. Additionally, we can deploy our softwares much easier and share our dev-environment in a couple of minutes. Just build the docker image and run the container. Let’s try.

Complete source code here

Comments

Copied title and URL