JumpStart Your Node.js Development

With over 87,000 stars and 3,100 contributors, Node.js has become a leading choice for enterprise developers in 2022. It’s an open source, cross-platform runtime environment that helps developers build varied server-side tools and applications in JavaScript. 

Developers use Node.js to build fast, scalable, and real-time apps — thanks to its highly-robust event-driven runtime. It’s also asynchronous. Node.js can handle a huge number of concurrent connections, with high throughput, as traffic spikes. Accordingly, it’s ideal for building microservices architectures. 

Event queue graphic

 

Users have downloaded our Node.js Docker Official Image more than 1 billion times from Docker Hub. What’s driving this significant download rate? There’s an ever-increasing demand for Docker containers to streamline development workflows, while giving Node.js developers the freedom to innovate with their choice of project-tailored tools, application stacks, and deployment environments. 

We’ll show you how to rapidly and easily containerize your Node.js app — seamlessly circumventing common Node compatibility issues while accelerating deployment. This lets your application easily run cross-platform on different CPU architectures. 

Building the Application       

Node js express

 

This walkthrough will show you how to easily build a Node.js to-do list app with Docker. 

First, we’ll create our simple to-do list application in Node.js without using Docker. You’ll see how the application lets you create and delete task lists using the Redis backend database. 

Next, we’ll build a Docker image for that application. You’ll also learn how Docker Compose can help you rapidly deploy your application within containers. Let’s get started. 

Prerequisites

  • NPM – a node package manager used for Node.js app development 
  • Node.js – our runtime for building web applications
  • Express – a backend web-application framework for Node.js 
  • Bootstrap – a toolkit for responsive, front-end web development
  • Redis – an in-memory, key-value, NoSQL database used for caching, data storage, and message brokering
  • Docker Desktopa suite of software-development tools for creating, sharing, and running individual containers

Getting Started

Once you’ve installed your Node.js packages on your machine, follow these steps to build a simple to-do list app from scratch.

Starting with NodeJS

  1.  Create an empty directory:

mkdir todolist

      2.   Run the npm init  command to set up a new npm package.

npm init

This utility walks you through creating a package.json file that describes your app and its dependencies.

 
{
  "name": "todolist",
  "version": "1.0.0",
  "description": "A Sample Todo-List app",
  "main": "todolist.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ajeetraina/todolist.git"
  },
  "keywords": [
    "node.js"
  ],
  "author": "Ajeet Singh Raina",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/ajeetraina/todolist/issues"
  },
  "homepage": "https://github.com/ajeetraina/todolist#readme"
}

With your new package.json, run npm install.

npm install --save express redis ejs dotenv

Here’s your result:

+ [email protected]
+ [email protected]
+ [email protected]
+ [email protected]
added 79 packages from 106 contributors and audited 79 packages in 5.112s

4 packages are looking for funding
run `npm fund` for details

found 0 vulnerabilities

Next, open your package.json file to see the following entries:

{
  "name": "todolist",
  "version": "1.0.0",
  "description": "A Sample Todo-List app",
  "main": "todolist.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ajeetraina/todolist.git"
  },
  "keywords": [
    "node.js"
  ],
  "author": "Ajeet Singh Raina",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/ajeetraina/todolist/issues"
  },
  "homepage": "https://github.com/ajeetraina/todolist#readme",
  "dependencies": {
    "dotenv": "^16.0.0",
    "ejs": "^3.1.6",
    "express": "^4.17.3",
    "redis": "^4.0.6"
  }
}

Installing nodemon

Nodemon is a handy CLI utility that’s primarily used for development purposes, instead of production. It monitors for any changes in your source code and automatically restarts the app server to apply them. Additionally, you can add nodemon to your dev dependencies if you want to run it using scripts. You can alternatively install it globally. Nodemon is open source and available on GitHub.

The recommended way to install nodemon is through the npm utility.

npm install --save-dev nodemon

Here’s your result:

+ [email protected]
added 106 packages from 55 contributors and audited 185 packages in 3.514s

18 packages are looking for funding
run `npm fund` for details

found 0 vulnerabilities

You should now be able to see nodemon added to your package.json file:

{
  "name": "todo-list",
  "version": "1.0.0",
  "description": "A Sample Todo List app",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app.js"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ajeetraina/todolist.git"
  },
  "keywords": [
    "nodejs",
    "express"
  ],
  "author": "Ajeet S Raina",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/ajeetraina/todolist/issues"
  },
  "homepage": "https://github.com/ajeetraina/todolist#readme",
  "dependencies": {
    "dotenv": "^16.0.0",
    "ejs": "^3.1.6",
    "express": "^4.17.3",
    "redis": "^4.0.6"
  },
  "devDependencies": {
    "nodemon": "^2.0.15"
  }
}

Defining the To-Do List Web App

First, create an empty app.js file that defines a web app using the Express.js framework. Next, you’ll have to ensure that all essential components are in place before initiating the build process. 

The first portion of your JavaScript code imports all essential packages into the project. However, you’ll also have to create an instance of an Express application along with redis, body-parser, and path. You then must initialize the express variable. Express is a minimal — but flexible — Node.js web-application framework with robust features for web and mobile applications. Express requires a middleware module called body-parser to extract incoming data from a POST request.

‘use strict’;

const express = require('express');
const redis = require('redis');
const path = require('path');
const port = 3000;
const bodyParser = require('body-parser');


var app = express();

const client = redis.createClient();

client.on('connect', () => {
  console.log('Successfully connected to Redis...');
});

Defining the View Engine

Next, you’ll need the view engine, which is useful for rendering web pages. You’ll also be using a popular view engine called Embedded JavaScript (EJS). This is essentially a simple templating language-engine that lets developers generate HTML using plain JavaScript. Your code section defines the path to the view engine. The bodyParser helps normalize each element.

The last line of this code snippet lets you insert an image under the /public directory — so that index.ejs can grab it and display it on the frontend:

app.set('views'), path.join(__dirname, 'views');
app.set('view engine', 'ejs');

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));

app.use(express.static(path.join(__dirname, 'public')))

GET

This code snippet uses Express router to handle your app’s routing. Whenever a user requests the app from a web browser, it’ll serve a response from the Redis database.

app.get('/', (req, res) => {
  var title = 'A Simple Todo-List App';
  var counter = 0;
  client.LRANGE('todo', 0, -1, (err, reply) => {
    if(err){
      res.send(err);
      return;
    }
    res.render('index', {
      title: title,
      todo: reply,
      counter: counter
    });
  });
});

Quick Tips: Redis lists can be defined as lists of strings that are stored in order of insertion:

  • RPUSH: adds a new element to the right of the list. (i.e. inserts the element at the tail of the list)
  • LRANGE: retrieves a subset of list elements based on the provided “start” and “stop” offsets

POST

Similarly, whenever you want to push data to the Redis database, you’d use the Redis RPUSH command as shown:

app.post('/todo/add', (req, res, next) => {
  var todo = req.body.todos;
  client.RPUSH('todo', todo, (err, reply) => {
    if(err){
      res.send(err);
      return;
    }
    res.redirect('/');
  });
});

Should you want to delete the messages in the Redis database by index, you’d use the LREM command. This Redis command removes the first-count occurrences of elements equal to elements from the list.

app.post('/todo/delete', (req, res, next) => {
  var delTODO = req.body.todo;
  var deleted = '__DELETED__';
  client.LRANGE('todo', 0, -1, (err, todo) => {
    for(let i = 0; i < delTODO.length; i++){
      client.LSET('todo', delTODO[i], deleted);
    }
    client.LREM('todo', 0, deleted);
    res.redirect('/');
  });
});

Meanwhile, this entry tells your app to start a server and listen for connections on port 3000:

app.listen(3000, () => {
  console.log('Server listening at port 3000...');
});

module.exports = app;

Here’s what your completed code will look like:

‘use strict’;

const express = require('express');
const redis = require('redis');
const path = require('path');
const port 3000
const bodyParser = require('body-parser');


var app = express();

const client = redis.createClient();

client.on('connect', () => {
  console.log('Successfully connected to Redis...');
});

app.set('views'), path.join(__dirname, 'views');
app.set('view engine', 'ejs');

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));

app.use(express.static(path.join(__dirname, 'public')));

app.get('/', (req, res) => {
  var title = 'A Simple Todo App List';
  var counter = 0;
  client.LRANGE('todo', 0, -1, (err, reply) => {
    if(err){
      res.send(err);
      return;
    }
    res.render('index', {
      title: title,
      todo: reply,
      counter: counter
    });
  });
});

app.post('/todo/add', (req, res, next) => {
  var todo = req.body.todos;
  client.RPUSH('todo', todo, (err, reply) => {
    if(err){
      res.send(err);
      return;
    }
    res.redirect('/');
  });
});

app.post('/todo/delete', (req, res, next) => {
  var delTODO = req.body.todo;
  var deleted = '__DELETED__';
  client.LRANGE('todo', 0, -1, (err, todo) => {
    for(let i = 0; i < delTODO.length; i++){
      client.LSET('todo', delTODO[i], deleted);
    }
    client.LREM('todo', 0, deleted);
    res.redirect('/');
  });
});

app.listen(3000, () => {
  console.log('Server listening at port 3000...');
});

module.exports = app;

Building a View Engine

The process of building a view engine is relatively simple. To do so, create an empty folder called “views” and add your content to a file namedindex.ejs:

!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="https://bootswatch.com/4/slate/bootstrap.min.css">
  <title>A Simple Todo List App</title>
</head>
<body>
  <div class="container">
    <h1 class="text-center"> <%= title %> </h1>
    <img src="/images/todo.jpeg" class="center"  width="300" alt="">
    <form action="/todo/add" method="POST">
      <div class="form-group">
        <input type="text" class="form-control" name="todos" placeholder="Start typing and Press Enter...">
      </div>
    </form>
    <form action="/todo/delete" method="POST">
    
      <% todo.forEach( (list) => { %>
        <div class="alert alert-success">
          <input type="checkbox" class="form-check-input mt-2" name="todo" value="<%= counter %>">
          <h4 class="d-inline">> </h4> <strong><%= list %></strong>
        </div>
        <% counter++ %>
      <% }) %>
        <input type="submit" value="Remove" class="btn btn-primary">
    </form>
    
  </div>
</body>
</html>

Next, let’s run the Redis server in your system. If you’re using Mac OS, we recommend using Homebrew as shown below:

brew install redis

brew services start redis

Once this is finished, verify that you are able to connect to Redis server via redis-cli:

redis-cli
127.0.0.1:6379> info
..

Finally, simply start nodemon with the following command. Your result is displayed soon after:

nodemon .

[nodemon] 2.0.12
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node .`
Server listening at port 3000...
Successfully connected to Redis...

You’ll then want to confirm that your app is working properly. Open your browser and access http://localhost:3000 in the address bar. Here’s what you’ll see:

A simple to do list app

 

Once you start typing out your to-do list, you’ll see the following activity in Redis database:

1650438955.239370 [0 172.17.0.1:59706] "info"
1650438999.498418 [0 172.17.0.1:59708] "info"
1650439059.072301 [0 172.17.0.1:59708] "lrange" "todo" "0" "-1"
1650439086.492042 [0 172.17.0.1:59708] "rpush" "todo" "Watch netflix"
1650439086.500834 [0 172.17.0.1:59708] "lrange" "todo" "0" "-1"
1650439094.054506 [0 172.17.0.1:59708] "rpush" "todo" "Attend Meeting"
1650439094.059366 [0 172.17.0.1:59708] "lrange" "todo" "0" "-1"
1650439099.726832 [0 172.17.0.1:59708] "rpush" "todo" "Walk for 30 min"
1650439099.731735 [0 172.17.0.1:59708] "lrange" "todo" "0" "-1"

Building a Multi-Container Node.js app with Docker Desktop

Let’s assess how you can run this app inside a Docker container using the official Docker image. First, you’ll need to install Docker Desktop — which lets you build a Docker image for your app.

Getting started with docker

Next, create an empty file called “Dockerfile”:

touch Dockerfile

Use your favorite text editor to open the Dockerfile. You’ll then need to define your base image. 

Accordingly, ensure that you’re using the Long Term Support (LTS) version of Node.js, and the minimal alpine image type. This helps minimize your image’s footprint, and therefore its attack surface:

FROM node:lts-alpine

Quick Tips: We recommended using explicit and deterministic Docker image base tags. Smaller Docker images offer quicker re-builds. Docker image builds are also highly varied. For example, if you use the node:latest tag with a build image, it’s possible that every build will try pulling a newly-built Docker node image. This could introduce non-deterministic behavior, which can hamper deployment consistency. 

Want to learn more about how Node.js and Docker can work together? Join us at DockerCon 2022 — where you’ll learn best practices for managing Node.js and JavaScript projects while developing, testing, and operating containers.

Next, let’s quickly create a directory to house our image’s application code. This acts as the working directory for your application:

WORKDIR '/var/www/app'

Both Node.js and NPM come pre-installed within your image. However, you’ll need to install your app dependencies using the npm binary. 

To bundle your app’s source code within your Docker image, use the COPY instruction:

COPY..

Your app binds to port 3000. Use the EXPOSE instruction to have the Docker daemon map it properly:

EXPOSE 3000

Your simple Dockerfile will look something like this:

FROM node:lts-alpine

COPY package*.json ./
  
WORKDIR '/var/www/app'

RUN npm install --save express redis ejs dotenv 

COPY . .

EXPOSE 3000

That said, you’ll now need to build your Docker image. Enter the following command to kickstart this process, which produces an output soon after:

docker build -t ajeetraina/todolist .

docker images
REPOSITORY                             TAG       IMAGE ID       CREATED         SIZE
ajeetraina/todolist                    latest    6aeeaac8ace3   2 minutes ago   131MB

Next, you’ll create a .dockerignore file. This file closely resembles.gitignore. It prevents files from being added to the initial build context — which the Docker daemon receives during docker build execution.

You’ll create this ..dockerignore file in the same directory as your Dockerfile with the following:

node_modules
.git

This prevents local modules and debug logs from being copied onto your Docker image. When this happens, you can potentially overwrite modules installed within your image.

If you rebuild the Docker image and verify it, you can save roughly 4MB of disk space:

docker images                        
REPOSITORY                             TAG       IMAGE ID       CREATED          SIZE
ajeetraina/todolist                    latest    453c5aeae5e0   3 seconds ago    127MB

Finally, it’s time to create a Docker Compose file. This single YAML file lets you specify your frontend app and your Redis database:

services:
  
  app:
    build: ./
    volumes:
       - ./:/var/www/app
    ports:
      - 3000:3000
    environment:
      - REDIS_URL=redis://db:6379
      - NODE_ENV=development
      - PORT=3000
    command:
       sh -c 'node app.js'
    depends_on:
      - db
 
  db:
    image: redis

Your example application has the following parts:

  • Two services backed by Docker images: your frontend web app and your backend database
  • The frontend, accessible via port 3000
  • The depends_on parameter, letting you create the backend service before the frontend service starts
  • One persistent volume, attached to the frontend

Next, you’ll specify your REDIS_URL within your app.js file — letting you pass Redis endpoints within your Docker Compose file:

const client = redis.createClient({ url: process.env.REDIS_URL });

 

You’ll then want to start your services using the docker-compose up command. Just like that, you’ve created and deployed your Node.js to-do list app! This is usable in your browser, like before:

Simple to do list app

 

Want to dig a little deeper? My complete project code is available on my GitHub page.

Conclusion

Docker helps accelerate the process of building, running, and sharing modern applications. Docker Official Images help you develop your own unique applications, no matter what tech stack you’re accustomed to. With a single YAML file, we demonstrated how Docker Compose helps you easily build a multi-container Node.js app with Redis.

We can even take Docker Compose and develop real-world microservices applications. With just a few extra steps, you can apply this tutorial while building applications with much greater complexity. Happy coding! 

Additional Resources