Dockerizing a NodeJS App

Converting a traditional NodeJS App into a Containerized App

In this post I am documenting what steps I made to convert a traditional NodeJS App that is launched from a command line using

node app.js

Into a fully dockerized container solution. The App uses a MySQL database which it has static configuration for. I am not going into too much details about the App’s code or architecture but it is just worth noting that it has this piece of configuration for connecting to the database;

connection: {
    host: localhost',
    user     : 'a-db-user',
    password : 'a-given-password',
    database : 'gengeni'
}

This means it is expecting to be given an address (in this case localhost) to the MySQL server instance. The first thing I had to do was to have the MySQL server running as a Docker container, this has several advantages in that there is no need to have MySQL installed and running on every machine that is used to develop, test or run a production instance of this App - only Docker needs to be installed.

This command starts a Docker container named mysql-main and gives it an environmental variable MYSQL_ROOT_PASSWORD which the image uses as the root password;

docker run --name mysql-main -e MYSQL_ROOT_PASSWORD=some-root-pass -d mysql
Unable to find image 'mysql:latest' locally
latest: Pulling from library/mysql
6d827a3ef358: Pull complete 
ed0929eb7dfe: Pull complete 
03f348dc3b9d: Pull complete 
fd337761ca76: Pull complete 
7e6cc16d464a: Pull complete 
ca3d380bc018: Pull complete 
23c12ddae61f: Pull complete 
14553c628372: Pull complete 
c9445076b453: Pull complete 
8feabd297745: Pull complete 
835cd3cde5d5: Pull complete 
Digest: sha256:3ea679cbde178e346dcdeb538fd1ea4f1af256020ebeb464ccb72a1646a2ba6d
Status: Downloaded newer image for mysql:latest
addc25b30a1d7e161c0a77ee8ab8767a529acd98183bb17b6f7e9bfe97912e12

Once this container is up and running, there are two ways to connect to it and run SQL commands.

  • By launching another container of the mysql image and use the mysql client
docker run -it --link mysql-main:mysql --rm mysql sh -c 'exec mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -uroot -p"$MYSQL_ENV_MYSQL_ROOT_PASSWORD"'
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.7.18 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.00 sec)
...
  • By using Docker to execute the bash shell of the same container (named maysql-main) I launched earlier and run the mysql client from there
docker exec -it mysql-main /bin/bash
root@addc25b30a1d:/# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 5
Server version: 5.7.18 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.00 sec)
...

With MySQL instance available as a docker container I loaded the database used by the App using SQL and the next step was to make the NodeJS App itself run in a container. The App could be started by running this command from the App’s root directory.

node app.js

But it could also be started by npm since it has a start script configured in the project’s package.json file

{
  "name": "gengeni",
  "version": "0.1.0",
  "description": "Gengeni API",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node app.js"
  },
  "repository": {
    "type": "git",
    "url": "....."
  },
  "author": "Haddad Said",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.14.0",
    "express": "^4.13.3",
    "knex": "^0.8.6",
    "moment": "^2.10.6",
    "mysql": "^2.13.0"
  }
}

That means it can also be started by the following command

npm start

So I created a Dockerfile and put the following in it

FROM node:latest 
WORKDIR /app
ADD . /app
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]

The file essentially does the following;

  1. Create an image based on the NodeJS Docker image
  2. Create a wroking directory /app and copy all files from the root directory of the project to that working directory. Basically we are including everything in our App’s file structure to the container image.
  3. Run ```npm install`` to install all dependencies
  4. Expose port 3000 which is what the App is litening to, by default Docker would block incoming connections into a container unless explicitly exposed with this direction.
  5. Run npm start to run the App when a container using this image is started.

Then I built an image using the Dockerfile by running this

docker build -t gengeni-api .

I now have the Docker images

docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
gengeni-api             latest              2d76b51e6692        2 hours ago        682MB
...
node                    latest              2ca756a6578b        5 days ago          665MB
mysql                   latest              d5127813070b        2 weeks ago         407MB
...

Then I started the App’s container

docker run gengeni-api
npm info it worked if it ends with ok
npm info using [email protected]
npm info using [email protected]
npm info lifecycle [email protected]~prestart: [email protected]
npm info lifecycle [email protected]~start: [email protected]

> [email protected] start /app
> node app.js

Gengeni API listening at http://:::3000
Knex:Error Pool2 - Error: connect ECONNREFUSED 127.0.0.1:3306
Knex:Error Pool2 - Error: connect ECONNREFUSED 127.0.0.1:3306

The container manages to start the App succesfully but the App fails to connect to the database because it is configured to look for it on localhost. So I changed the App’s configuration to be as follows

connection: {
    host: process.env.DATABASE_HOST,
    user     : 'a-db-user',
    password : 'a-given-password',
    database : 'gengeni'
}

This way it will use the enironmental variable DATABASE_HOST to work out the hostname of the database. Then I luanched the App’s container using this command

docker run --link mysql-main:db -e DATABASE_HOST=db -p 3000:3000 gengeni-api
npm info it worked if it ends with ok
npm info using [email protected]
npm info using [email protected]
npm info lifecycle [email protected]~prestart: [email protected]
npm info lifecycle [email protected]~start: [email protected]

> [email protected] start /app
> node app.js

Gengeni API listening at http://:::3000

Now the App is connecting to the MySQL database successfuly. Making an HTTP request on the exposed port 3000 returns the expected results;

http localhost:3000/users
HTTP/1.1 200 OK
Access-Control-Allow-Headers: origin, content-type, accept
Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, HEAD
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 2
Content-Type: application/json; charset=utf-8
Date: Wed, 26 Apr 2017 12:31:32 GMT
ETag: W/"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w"
X-Powered-By: Express

[]


 
comments powered by Disqus