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.
- Start Docker from scratch
- Docker volume
- Bind host directory to Docker container for dev-env
- Communication with other Docker containers
- Run multi Docker containers with compose file
- Container’s dependency check and health check
- Override Docker compose file to have different environments
- Creating a cluster with Docker swarm and handling secrets
- Update and rollback without downtime in swarm mode
- Container optimization
- Visualizing log info with Fluentd, Elasticsearch and Kibana
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.
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
-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
$ 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.
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
/src/hello-world |--Dockerfile |--lib | |--app.ts |--package.json |--tsconfig.json
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 -gis 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 > firstname.lastname@example.org 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
$ 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
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!
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.
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.