In this post, we are going to look at developing a Node.js application using Docker as the development environment.
Why Dockerize my development?
You may ask, what is wrong with my current setup? Why should I “Dockerize my Node app”?
Well for starters, your development environment need not be the machine on which you are coding.
Let’s say that you are new to a project and you are getting started with your environment setup and you are installing an awesome plugin like grunt-contrib-imagemin and you find that its dependency libpng is missing on your machine. You Google and try to solve the problem yourself because a. you are the new member in the team and you want to prove yourself and b. you don’t know anyone in the team yet.
Everything is good so far, you were able to fix the libpng issue and now you run
npm install
again and you notice that grunt-yellowlabtools needs phantomjs and for some reason the download fails while installation. Again for the above said reasons, you are too shy to approach someone so you spend the first day on the project installing the dependencies and finally setting up your environment and running your project successfully.
Very productive day one.
Now, imagine, one your first day, all you would need to do is
- Install Docker
- Run
docker run -it -p 3000:3000 myproject/my-app:0.1.0
And voila, the app is running on your machine. And you are all set to get started. How about that?
4 reasons why?
Here are a few reasons why you need a “Dockerized” development environment:
- Your production environment will almost always never be same as your development environment. So you can directly work on your production environment setup on your local to avoid “surprising” deployment issues.
- Easy for your devops team to work and scale with containers in higher environments than your local
- One developer machine can run multiple (Node.js) applications in an isolated way
- Docker compose lets us run dependent pieces of softwarer in an micro service architecture easily
Now that you feel it is a good idea to Dockerize your development environment, lets look at how.
How to Dockerize a Node.js app?
We are going to follow below steps
- Setup Docker
- Build a simple Node.js application
- Build a Docker image
- Create a container from the Image and start developing.
Before we actually get started, let’s look at what is Docker.
What is Docker?
Docker is a computer program that performs operating-system-level virtualization also known as containerization.
To understand the above jibber-jabber, take a look at this video
Quite a simple and powerful concept.
If you would like to dig deeper in the world of containers, checkout this playlist from Vmware
Now that we understand the why, what and how; let’s get our hands dirty by building a Dockerized Node.js app.
Getting Started
First we are going to install Docker
Install Docker
For this tutorial, we are going to use Docker Community Edition (CE). To Install Docker on our machine, navigate to here & download the software for your OS.
Once Installed, you can open up a command prompt/terminal and run
docker ps
and you should see something like
➜ ~ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
The above output shows that there are no images running.
Now, lets quickly test drive Docker.
Run
docker run busybox echo "hello from busybox"
and you should see that the busybox image downloaded locally and run the
echo command that we have provided. The output would be as follows.
Now that we got the Docker installation done & verified, let’s move on.
Build a Node.js App
Now, we will setup a simple Node.js app. If you are planning to run this application on your local machine, make sure Node.js is setup on your machine. You can follow this post Hello Node. I am using the following versions
➜ ~ node -v
v8.11.1
➜ ~ npm -v
5.6.0
If you are going to run the app directly in a docker container, you can skip local installation of Node.js.
Anywhere on your machine, create a folder named
docker-node-app
and open a new terminal/prompt there.
First, we are going to initialise a new Node.js project. Run
➜ docker-node-app npm init --yes
This will create a
package.json
with default contents. Update it as shown below, as applicable
{
"name": "docker-node-app",
"version": "1.0.0",
"description": "A node.js app that runs inside a docker contianer but lives outside",
"main": "index.js",
"scripts": {
"start": "nodemon index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": "https://github.com/arvindr21/",
"keywords": ["docker", "nodejs", "dev", "hotreloading"],
"author": "Arvind Ravulavaru <[email protected]> (http://thejackalofjavascript.com)",
"license": "MIT"
}
Do note that I have added a script named
start
, which launches the app that we are going to create in a moment.
Now, we are going to build a simple app, which prints the environment variables on the machine it is running.
Inside
docker-node-app
folder, create a file named
index.js
and update it as shown below
const http = require('http');
const port = process.env.PORT || 3000;
let server = http.createServer((req, res) => {
let html = `<h1>Environment Variables</h1><br>`;
// Iterate over `process.env` object and
// print its values.
Object.keys(process.env).forEach((k) => {
html += `${k} = ${process.env[k]} <br>`
});
// Set the response status and response content
// type header
res.writeHead(200, {
'content-type': 'text/html'
});
return res.end(html);
});
// start listening
server.listen(port);
console.log(`Server running on port ${port}`);
All we are doing is iterating the
process.env
object & building a string with the key and value.
Test drive the Node.js application
Let’s test the application we have built. You can do this only if you have Node.js installed on your machine.
You can simply run
➜ docker-node-app node index.js
Server running on port 3000
or
➜ docker-node-app nodemon index.js
[nodemon] 1.17.5
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node index.js`
Server running on port 3000
If you do not have nodemon on your machine, you can install it as follows
➜ ~ npm install nodemon --global
Once the server is up and running, navigate to http://localhost:3000/, we should see
Isni’t very brave of me to publish my machine’s environment variables out in public :/
Now that we have validated that the app is working fine, lets “containerize” it.
Build a Docker image
Now that we have our sample app running, let’s create an image. Inside
docker-node-app
folder, create a file named
Dockerfile
and update it as shown below
# A node.js v8 box
FROM node:8
# Who(m) to blame if nothing works
MAINTAINER [email protected]
# Create a working directory
RUN mkdir -p /usr/src/app
# Switch to working directory
WORKDIR /usr/src/app
# Copy contents of local folder to `WORKDIR`
# You can pick individual files based on your need
COPY . .
# Install nodemon globally
RUN npm install -g nodemon
# Install dependencies (if any) in package.json
RUN npm install
# Expose port from container so host can access 3000
EXPOSE 3000
# Start the Node.js app on load
CMD [ "npm", "start" ]
On line 2 : we set the base image in which our app is going to run. Which is a Node image with version 8 installed.
On line 8: we create a directory where our source code would reside in the container
On line 11: we switch to the working directory
On line 15: we copy our current source code to the image
On line 18: we install nodemon globally.
On line 21: we install any other dependencies defined in
package.json
On line 24: we expose port 3000 so the host machine can access the app
On line 27: we start the Node.js app on boot of the container
Simple right?
Now to make sure we copy only what we need, we will create a file named
.dockerignore
at the root of
docker-node-app
and update it as shown below
node_modules
npm-debug.log
Now that we are done with setup, we will build the docker image. From inside
docker-node-app
folder, run
➜ docker-node-app docker build -t arvindr21/docker-node-app:0.1.0 .
Do note that
arvindr21
from the above command is my Dockerhub username. If you are planning to push this image to Dockerhub, it needs to be with your Dockerhub username.
Once the build kicks off, you should see logs as shown below
Sending build context to Docker daemon 7.68kB
Step 1/9 : FROM node:8
8: Pulling from library/node
1c7fe136a31e: Pull complete
ece825d3308b: Pull complete
06854774e2f3: Pull complete
f0db43b9b8da: Pull complete
aa50047aad93: Pull complete
42b3631d8d2e: Pull complete
93c1a8d9f4d4: Pull complete
5fe5b35e5c3f: Pull complete
Digest: sha256:420104c1267ab7a035558b8a2bc13539741831ac4369954031e0142b565fb7b5
Status: Downloaded newer image for node:8
---> ba6ed54a3479
Step 2/9 : MAINTAINER [email protected]
---> Running in 90a25907fd58
Removing intermediate container 90a25907fd58
---> 260d8c31364f
Step 3/9 : RUN mkdir -p /usr/src/app
---> Running in ace890e16232
Removing intermediate container ace890e16232
---> 4651ae6b3eb8
Step 4/9 : WORKDIR /usr/src/app
Removing intermediate container 775bf6bf0c6e
---> 9123d4de2b66
Step 5/9 : COPY . .
---> 2213441f094e
Step 6/9 : RUN npm install -g nodemon
---> Running in c73bb8b92ea9
/usr/local/bin/nodemon -> /usr/local/lib/node_modules/nodemon/bin/nodemon.js
> [email protected] postinstall /usr/local/lib/node_modules/nodemon
> node bin/postinstall || exit 0
Love nodemon? You can now support the project via the open collective:
> https://opencollective.com/nodemon/donate
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: [email protected] (node_modules/nodemon/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for [email protected]: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
+ [email protected]
added 232 packages in 14.401s
Removing intermediate container c73bb8b92ea9
---> 6f56d36ee417
Step 7/9 : RUN npm install
---> Running in b99429de7651
up to date in 0.079s
Removing intermediate container b99429de7651
---> c0437cefd284
Step 8/9 : EXPOSE 3000
---> Running in 66b80b41343b
Removing intermediate container 66b80b41343b
---> 2011d0ceb321
Step 9/9 : CMD [ "npm", "start" ]
---> Running in 330202b704db
Removing intermediate container 330202b704db
---> 39a06e589954
Successfully built 39a06e589954
Successfully tagged arvindr21/docker-node-app:0.1.0
Now, if we run
➜ docker-node-app docker images
we should see
➜ docker-node-app docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
arvindr21/docker-node-app 0.1.0 39a06e589954 35 seconds ago 681MB
Awesome! Now our image is ready. We need to create a container from this image and voila our app will be running from inside an image.
Create a conatiner & Run the app
This is the most important step in this process.
To run our Node.js app in a container, we are going to
- Download the code base on our local machine
- Point the volume of the container to the folder where the code is downloaded and run the image
- Open the local copy of code on your host machine text editor
- Start making changes
So, let’s move on. Since the codebase is already on our machine, we are not going to download it. If you project files are hosted on a remote server, download them to your local.
Next, we are going to create a container from the image and point the volume to
docker-node-app
folder, as shown below
➜ docker-node-app docker run -it -p 3000:3000 -v ${PWD}:/usr/src/app arvindr21/docker-node-app:0.1.0
/usr/src/app
: is the folder in the container
${PWD}
: is the folder on the host machine where the code is present. I am running the above command from the same folder, hence I am passing in the present working directory variable
3000
: is the port mapping between host machine and container.
Note: If you want to run this app as a background process pass a
-d
or a deamon flag as shown below
➜ docker-node-app docker run -itd -p 3000:3000 -v ${PWD}:/usr/src/app arvindr21/docker-node-app:0.1.0
If everything goes well, we should see
➜ docker-node-app docker run -it -p 3000:3000 -v ${PWD}:/usr/src/app arvindr21/docker-node-app:0.1.0
> [email protected] start /usr/src/app
> nodemon index.js
[nodemon] 1.17.5
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node index.js`
Server running on port 3000
Now, navigate to http://localhost:3000/ and we should see something like
Do notice that the values now have updated to the environment variables from the container. Do notice the
HOSTNAME
property, it is displaying a value of
cb0852847690
. Now, from the host machine, run
docker ps
and we should see something like
➜ docker-node-app docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cb0852847690 arvindr21/docker-node-app:0.1.0 "npm start" 3 minutes ago Up 3 minutes 0.0.0.0:3000->3000/tcp blissful_gates
Do not the
HOSTNAME
matches the
CONTAINER ID
.
So, ya, it works.
Now to the awesome part, live reloading.
Live reload the container app
Now, we have our app running in a container and the code base on our host machine, let’s add a change to the code base on our local machine and see the changes reflect in the container app.
Open
index.js
and update it as shown below
const http = require('http');
const port = process.env.PORT || 3000;
let server = http.createServer((req, res) => {
let html = `<h1>Environment Variables</h1>`;
// Add new code to test the live reload
html += `<h2>As of: ${new Date()}</h2><br>`;
// Iterate over `process.env` object and
// print its values.
Object.keys(process.env).forEach((k) => {
html += `${k} = ${process.env[k]} <br>`
});
// Set the response status and response content
// type header
res.writeHead(200, {
'content-type': 'text/html'
});
return res.end(html);
});
// start listening
server.listen(port);
console.log(`Server running on port ${port}`);
I have added a new piece of code to display the time.
Save the file on your host machine and we should see the following in the terminal/prompt
➜ docker-node-app docker run -it -p 3000:3000 -v ${PWD}:/usr/src/app arvindr21/docker-node-app:0.1.0
> [email protected] start /usr/src/app
> nodemon index.js
[nodemon] 1.17.5
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node index.js`
Server running on port 3000
[nodemon] restarting due to changes...
[nodemon] starting `node index.js`
Server running on port 3000
Now, back to http://localhost:3000/ and we should see
Voila! The time appear-th! Keep refreshing to see the value change.
Now, when you are done with the development, you can push the code to the remote repository and shutdown your Docker image. And once when you are ready to code, you can bring up the Docker image.
Adding code files
Okay, so we are able to make changes to an existing file, what about adding a new file. Let’s try that out.
Inside
docker-node-app
folder, create a file called as
date.js
and update it as shown below
module.exports = function getDate() {
return new Date();
}
All we have done is externalized the logic to get current date. Now, open
index.js
and update it as shown below
const http = require('http');
const port = process.env.PORT || 3000;
const getDate = require('./date.js');
let server = http.createServer((req, res) => {
let html = `<h1>Environment Variables</h1>`;
// Add new code to test the live reload
html += `<h2>As of: ${getDate()}</h2><br>`;
// Iterate over `process.env` object and
// print its values.
Object.keys(process.env).forEach((k) => {
html += `${k} = ${process.env[k]} <br>`
});
// Set the response status and response content
// type header
res.writeHead(200, {
'content-type': 'text/html'
});
return res.end(html);
});
// start listening
server.listen(port);
console.log(`Server running on port ${port}`);
And you should see a message in the prompt/terminal that the server restarted due to changes. Now if we go back to browser and refresh, we should see that the output does not change but we are loading the date from an external file.
Simple and powerful development environment that is development and deployment friendly.
You can find the code used in this post here: arvindr21/docker-node-app
Thanks for reading! Do comment.
@arvindr21
The post Developing Node.js applications in Docker appeared first on The Jackal of Javascript.