Docker
Posted By Sebastian

Docker – Beginner’s Guide – Part 2: Services


Subscribe On YouTube

 

Docker Beginner’s Guide – Part 1: Images & Containers
Docker Beginner’s Guide – Part 2: Services

So far you’ve learned how to deal with Docker images and containers to run application in a container environment. However in most real-world scenarios an “app” consists of different pieces. E.g. a web application is requiring that a web server is running which is serving content to the browser. In addition a database server is needed and the web application is connecting to the database to fetch data.

In Docker you’re able to orchestrate those “services” by combining multiple images. By using that approach we’re able to define one service which is running the MongoDB database and another service which is running the web application.

With Docker it’s very easy to define and run those services – we just need to write a docker-compose.yml file in which services are declared.

However before doing so let’s start with a real-world example from scratch: In the next steps we’re creating a Node.js server application which is exposing a REST API for managing Todo items. By using the Mongoose library the Node.js server is connecting to a MongoDB database which is responsible for persisting the data.

To let this app run on Docker we’re writing a docker-compose.yml file and define two services: one service for running the Node.js server and another server for running the MongoDB database. Because the Node.js server application needs to connect to the MongoDB database we’ll also learn how to connect one Docker service with another.

Let’s get started:

Creating The Node.js App

First we need to create a new empty project folder:

$ mkdir node_app

Change into the newly created project folder:

$ cd node_app

and make sure that a new empty package.json file is initialised by executing:

$ npm init -y

Now we’re able to install needed dependencies by using the Node.js Package Manager (Node.js needs to be installed on your system):

$ npm install express body-parser mongoose

With those prerequisites in place we’re ready to start implementing the Node.js server application.

Create a new file index.js and insert the following JavaScript code:

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const todoRoutes = express.Router();
const PORT = 4000;

let Todo = require('./todo.model');

app.use(bodyParser.json());

mongoose.connect('mongodb://mongo:27017/todos', { useNewUrlParser: true });
const connection = mongoose.connection;

connection.once('open', function() {
    console.log("MongoDB database connection established successfully");
})

todoRoutes.route('/').get(function(req, res) {
    Todo.find(function(err, todos) {
        if (err) {
            console.log(err);
        } else {
            res.json(todos);
        }
    });
});

todoRoutes.route('/:id').get(function(req, res) {
    let id = req.params.id;
    Todo.findById(id, function(err, todo) {
        res.json(todo);
    });
});

todoRoutes.route('/update/:id').post(function(req, res) {
    Todo.findById(req.params.id, function(err, todo) {
        if (!todo)
            res.status(404).send("data is not found");
        else
            todo.todo_description = req.body.todo_description;
            todo.todo_responsible = req.body.todo_responsible;
            todo.todo_priority = req.body.todo_priority;
            todo.todo_completed = req.body.todo_completed;

            todo.save().then(todo => {
                res.json('Todo updated!');
            })
            .catch(err => {
                res.status(400).send("Update not possible");
            });
    });
});

todoRoutes.route('/add').post(function(req, res) {
    let todo = new Todo(req.body);
    todo.save()
        .then(todo => {
            res.status(200).json({'todo': 'todo added successfully'});
        })
        .catch(err => {
            res.status(400).send('adding new todo failed');
        });
});

app.use('/todos', todoRoutes);

app.listen(PORT, function() {
    console.log("Server is running on Port: " + PORT);
});

This code is needed to setup the Node.js server on port 4000 and connect to the MongoDB database by using Mongoose. If you’re not familiar with either JavaScript or Node.js you can just copy and paste the code. However, a few things to note:

  • The connection string which is used to connect to MongoDB is mongodb://mongo:27017/todos. This means that we need to ensure that a service with name mongo is running which is exposing access to a MongoDB instance at port 27017.
  • Four API endpoints are exposed:
    • /todos/ – accepting HTTP GET requests and returning todo items from the database
    • /todos/:id – accepting HTTP GET requests and returning a specific todo item based on the ID
    • /todos/update/:id – accepting HTTP POST requests to update existing todo items
    • /todos/add – accepting HTTP POST requests to create new todo items

Furthermore we need to define the Mongoose model in file todo.model.js:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

let Todo = new Schema({
    todo_description: {
        type: String
    },
    todo_responsible: {
        type: String
    },
    todo_priority: {
        type: String
    },
    todo_completed: {
        type: Boolean
    }
});

module.exports = mongoose.model('Todo', Todo);

Next create a .dockerignore file inside the project folder and insert the following line:

node_modules

Therewith we’re making sure that the content of the node_modules folder is not copied into the image which we’re going to create.

Creating a Custom Image For Our Node Application

To create the custom image the following Dockerfile needs to be created:

FROM node
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . . 
EXPOSE 4000
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.2.1/wait /wait
RUN chmod +x /wait
CMD /wait && npm start

The custom image is created based on the official node image. First we’re making sure that the package.json file (which is containing needed dependencies) is copied to the image. Then we’re running the npm install command to download all dependencies into the image.

In the next step we’re making sure that the files of our project are copied into the current folder of the image as well. Then we’re exposing port 4000 because that is the port on which the Node.js server application is running.

In the next step another package is downloaded from https://github.com/ufoscout/docker-compose-wait/releases/download/2.2.1/wait. This is simply command line utility to wait for other docker images to be started while using docker-compose. We need to make use of this utility to delay the start of the Node.js server until the MongoDB database instance (which is starting using another Docker image) is ready and available. We need to make the wait utility executable on the command line by adding the following line of code to our Dockerfile:

RUN chmod +x /wait

Finally we’re ready to run the wait utility and execute the server by running npm start by adding the following line:

CMD /wait && npm start

Now that we’ve completed a Dockerfile which can be used to create a custom Docker image which is container a Node.js environment together with our Node.js server application we’re ready to compose our services.

Composing Docker Services

Composing Docker services is done in a new file docker-compose.yml in the project directory:

version: '3'
services: 
  node_app:
    container_name: node_app
    restart: always
    build: .
    ports: 
      - '4000:4000'
    links:
      - mongo
    environment:
      WAIT_HOSTS: mongo:27017
  mongo:
    container_name: mongo
    image: mongo
    ports: 
      - '27017:27017'
    volumes:
      - './data/db:/data/db'

Let’s break this down:

  • Two services are being defined: app and mongo
  • For the node_app service we’re defining a container name of node_app and for the mongo service we’re defining a container name of mongo. By assigning custom container names we avoid dealing with randomly generated container names for both services.
  • By assigning the value always to the restart property of the node_app service we’re making sure that the container is restarted automatically if it fails.
  • The node_app image is build using the Dockerfile we just added to the current folder (build: .)
  • The host port 4000 (which is exposed by the image) is mapped to the container port 4000 for the node_app service.
  • The container for the mongo service is build with the official MongoDB image mongo.
  • The host port 27027 is mapped to the container port 27027 for the mongo service. That’s the standard port which is used for accessing the MongoDB database.
  • The mongo service is linked to the node_app service. So that from inside of the node_app container we’re able to access the MongoDB database by using the connection string described above.
  • The environment variable WAIT_HOSTS is set to mongo:27017 for the node_app service. This is telling the wait script to wait for port 27017 of the mongo service.

Running Services

With the docker-compose.yml file in place we’re ready to start up the service. Therefore you need to use the following command in the folder in which the docker-compose.yml file is stored:

$ docker-compose up

In the output on the command line you then should see that both services are being started (node_app and mongo).

Finally you should be able to see the out output which is comming from our Node.js server application: “MongoDB database connection established successfully”.

Testing The Application

Both services are up and running so that we’re ready to test the API which is exposed. Because we need to be able to send HTTP request to the server we’re making use of the Postman tool (https://www.getpostman.com/).

The first test is to send an HTTP request to endpoint http://localhost:4000/todos. Because the MongoDB database is still empty we’re getting returned an empty array as the response:

Next we’re sending an HTTP POST request to endpoint http://localhost:4000/add to create a new todo item in the MongoDB database. The data is sent in the request body in JSON format:

Sending another GET request to http://localhost:4000/todos should now return an array which comprises one entry:

Stopping Services

The services which we’ve started with docker-compose up are still running. You can always see an overview of running services by typing in the following command:

$ docker-compose ps

The result is a list of services. The status Up indicates that the service is active right now:

Use option stop to stop running services:

$ docker-compose stop

After services have stopped you can use the option rm to delete service containers:

$ docker-compose rm

Both (stop and delete) can also be done in one step by using the following command:

$ docker-compose down

Top 3 Docker Online Courses


Using and writing about best practices and latest technologies in web design & development is my passion.