Dockerizing a Node.js Web Application

What is Docker, Anyway?

“Docker is an open platform for building, shipping and running distributed applications. It gives programmers, development teams and operations engineers the common toolbox they need to take advantage of the distributed and networked nature of modern applications.”

Advantages of Using Docker

Prerequisites

Create Repository

$ git clone YOUR_REPOSITORY_URL
$ cd YOUR_REPOSITORY_NAME

Directory Structure

$ npx express-generator --no-view addressbook
$ cd addressbook
$ npm install

Add a Route

// persons.js

var express = require('express');
var router = express.Router();
var db = require('../database');

router.get("/all", function(req, res) {
db.Person.findAll()
.then( persons => {
res.status(200).send(JSON.stringify(persons));
})
.catch( err => {
res.status(500).send(JSON.stringify(err));
});
});

router.get("/:id", function(req, res) {
db.Person.findByPk(req.params.id)
.then( person => {
res.status(200).send(JSON.stringify(person));
})
.catch( err => {
res.status(500).send(JSON.stringify(err));
});
});

router.put("/", function(req, res) {
db.Person.create({
firstName: req.body.firstName,
lastName: req.body.lastName,
id: req.body.id
})
.then( person => {
res.status(200).send(JSON.stringify(person));
})
.catch( err => {
res.status(500).send(JSON.stringify(err));
});
});

router.delete("/:id", function(req, res) {
db.Person.destroy({
where: {
id: req.params.id
}
})
.then( () => {
res.status(200).send();
})
.catch( err => {
res.status(500).send(JSON.stringify(err));
});
});

module.exports = router;
router.put("/", function(req, res) {
db.Person.create({
firstName: req.body.firstName,
lastName: req.body.lastName,
id: req.body.id
})
.then( person => {
res.status(200).send(JSON.stringify(person));
})
.catch( err => {
res.status(500).send(JSON.stringify(err));
});
});
router.delete("/:id", function(req, res) {
db.Person.destroy({
where: {
id: req.params.id
}
})
.then( () => {
res.status(200).send();
})
.catch( err => {
res.status(500).send(JSON.stringify(err));
});
});
module.exports = router;

Configuring the Database

$ npm install --save pg sequelize
// database.js

const Sequelize = require('sequelize');
const sequelize = new Sequelize(process.env.DB_SCHEMA || 'postgres',
process.env.DB_USER || 'postgres',
process.env.DB_PASSWORD || '',
{
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
dialectOptions: {
ssl: process.env.DB_SSL == "true"
}
});
const Person = sequelize.define('Person', {
firstName: {
type: Sequelize.STRING,
allowNull: false
},
lastName: {
type: Sequelize.STRING,
allowNull: true
},
});
module.exports = {
sequelize: sequelize,
Person: Person
};
// bin/migrate.js

var db = require('../database.js');
db.sequelize.sync();
$ npm install --save-dev jest
const db = require('./database');

beforeAll(async () => {
await db.sequelize.sync({ force: true });
});

test('create person', async () => {
expect.assertions(1);
const person = await db.Person.create({
id: 1,
firstName: 'Bobbie',
lastName: 'Draper'
});
expect(person.id).toEqual(1);
});

test('get person', async () => {
expect.assertions(2);
const person = await db.Person.findByPk(1);
expect(person.firstName).toEqual('Bobbie');
expect(person.lastName).toEqual('Draper');
});

test('delete person', async () => {
expect.assertions(1);
await db.Person.destroy({
where: {
id: 1
}
});
const person = await db.Person.findByPk(1);
expect(person).toBeNull();
});

afterAll(async () => {
await db.sequelize.close();
});
"scripts": {
"start": "node ./bin/www",
"test": "jest",
"migrate": "node ./bin/migrate.js"
},

Start the Application

// app.js

. . .

var indexRouter = require('./routes/index');

// add the following line near the indexRouter
var personsRouter = require('./routes/persons');

. . .
// app.js

. . .

app.use('/', indexRouter);

// add the following line near app.use indexRouter
app.use('/persons', personsRouter);

. . .
$ npm start

Setting Up PM2

$ npm install --save pm2
{
"apps": [
{
"name": "api",
"script": "./bin/www",
"merge_logs": true,
"max_restarts": 20,
"instances": 4,
"max_memory_restart": "200M",
"env": {
"PORT": 3000,
"NODE_ENV": "production"
}
}
]
}
"scripts": {
"pm2": "pm2 start processes.json --no-daemon",
"start": "node ./bin/www",
"test": "jest",
"migrate": "node ./bin/migrate.js"
},
$ npm run pm2

Installing Docker

Running Postgres With Docker

$ docker run -it -e "POSTGRES_HOST_AUTH_METHOD=trust" -p 5432:5432 postgres
$ npm run migrate
$ npm run pm2
$ npm run test

> addressbook@0.0.0 test /home/tom/r/dockerizing-test/addressbook
> jest

PASS ./database.test.js
✓ create person (18ms)
✓ get person (6ms)
✓ delete person (7ms)

console.log node_modules/sequelize/lib/sequelize.js:1187
Executing (default): CREATE TABLE IF NOT EXISTS "People" ("id" SERIAL , "firstName" VARCHAR(255) NOT NULL, "lastName" VARCHAR(255), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY ("id"));

console.log node_modules/sequelize/lib/sequelize.js:1187
Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'People' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;

console.log node_modules/sequelize/lib/sequelize.js:1187
Executing (default): INSERT INTO "People" ("id","firstName","lastName","createdAt","updatedAt") VALUES ($1,$2,$3,$4,$5) RETURNING *;

console.log node_modules/sequelize/lib/sequelize.js:1187
Executing (default): SELECT "id", "firstName", "lastName", "createdAt", "updatedAt" FROM "People" AS "Person" WHERE "Person"."id" = 1;

console.log node_modules/sequelize/lib/sequelize.js:1187
Executing (default): DELETE FROM "People" WHERE "id" = 1

console.log node_modules/sequelize/lib/sequelize.js:1187
Executing (default): SELECT "id", "firstName", "lastName", "createdAt", "updatedAt" FROM "People" AS "Person" WHERE "Person"."id" = 1;

Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 1.119s
Ran all test suites.

Creating a Dockerfile

$ cd ..
# Dockerfile

FROM node:16.15-alpine3.14
RUN mkdir -p /opt/app
WORKDIR /opt/app
RUN adduser -S app
COPY addressbook/ .
RUN npm install
RUN npm install --save pm2
RUN chown -R app /opt/app
USER app
EXPOSE 3000
CMD [ "npm", "run", "pm2" ]
.git
.gitignore
node_modules/

Bundling and Running the Docker Container

$ docker build -t addressbook .
$ docker run -it -p 3000:3000 addressbook

Docker Compose

# docker-compose.yml

version: "3.9"
services:
postgres:
image: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- '5432:5432'
volumes:
- addressbook-db:/var/lib/postgresql/data

addressbook:
build:
context: .
environment:
DB_SCHEMA: postgres
DB_USER: postgres
DB_PASSWORD: postgres
DB_HOST: postgres
depends_on:
- postgres
ports:
- '3000:3000'

volumes:
addressbook-db:
$ docker ps
$ docker compose run addressbook npm test

Creating dockerizing-nodejs_postgres_1 … done
Creating dockerizing-nodejs_addressbook_run … done
addressbook@0.0.0 test /opt/app
jest
PASS ./database.test.js
✓ create person (21ms)
✓ get person (10ms)
✓ delete person (10ms)
Executing (default): DROP TABLE IF EXISTS "People" CASCADE; Executing (default): DROP TABLE IF EXISTS "People" CASCADE; Executing (default): CREATE TABLE IF NOT EXISTS "People" ("id" SERIAL , "firstName" VARCHAR(255) NOT NULL, "lastName" VARCHAR(255), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY ("id")); Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'People' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname; Executing (default): INSERT INTO "People" ("id","firstName","lastName","createdAt","updatedAt") VALUES ($1,$2,$3,$4,$5) RETURNING *; Executing (default): SELECT "id", "firstName", "lastName", "createdAt", "updatedAt" FROM "People" AS "Person" WHERE "Person"."id" = 1; Executing (default): DELETE FROM "People" WHERE "id" = 1 Executing (default): SELECT "id", "firstName", "lastName", "createdAt", "updatedAt" FROM "People" AS "Person" WHERE "Person"."id" = 1;
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 2.475s
Ran all test suites.
$ docker compose up -d
$ curl -w "\n" \
-X PUT \
-d "firstName=Bobbie&lastName=Draper" \
localhost:3000/persons
$ curl -w "\n" localhost:3000/persons/all

[
{
"id": 1,
"firstName": "Bobbie",
"lastName": "Draper",
"createdAt": "2020-02-07T23:52:00.448Z",
"updatedAt": "2020-02-07T23:52:00.448Z"
}
]
$ git add -A
$ git commit -m "initial commit"
$ git push origin master

Dockerize the Node application with CI/CD

checkout
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
docker pull "${DOCKER_USERNAME}/dockerizing-nodejs-addressbook:latest" || true
docker build --cache-from "${DOCKER_USERNAME}/dockerizing-nodejs-addressbook:latest" -t "${DOCKER_USERNAME}/dockerizing-nodejs-addressbook:latest" .
docker push "${DOCKER_USERNAME}/dockerizing-nodejs-addressbook:latest"

Testing the Dockerized Node Application Image

$ git pull origin master
# docker-compose.ci.yml

version: "3.9"
services:
postgres:
image: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- '5432:5432'

addressbook:
image: $DOCKER_USERNAME/dockerizing-nodejs-addressbook:latest
command: "npm run migrate && npm run pm2"
environment:
DB_SCHEMA: postgres
DB_USER: postgres
DB_PASSWORD: postgres
DB_HOST: postgres
depends_on:
- postgres
ports:
- '3000:3000'
FROM registry.semaphoreci.com/node:16
RUN mkdir -p /opt/app
WORKDIR /opt/app
RUN adduser app
COPY addressbook/ .
RUN npm install
RUN npm install --save pm2
RUN chown -R app /opt/app
USER app
EXPOSE 3000
CMD [ "npm", "run", "pm2" ]
$ git pull origin master
$ git add docker-compose.ci.yml Dockerfile
$ git commit -m "add docker compose and Semaphore registry"
$ git push origin master
docker-compose run addressbook npm test
checkout
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
cat docker-compose.ci.yml | envsubst | tee docker-compose.yml
$ docker pull YOUR_DOCKER_USERNAME/dockerizing-nodejs-addressbook:latest

Next Steps

Conclusion

--

--

Supporting developers with insights and tutorials on delivering good software. · https://semaphoreci.com

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store