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;
- Create an image based on the NodeJS Docker image
- 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.
- Run ```npm install`` to install all dependencies
- 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.
- 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
[]