Wednesday, May 20, 2015

Series: How to create your own website based on Docker (Part 8 - Creating the ioJS REST API Docker container)

It's about time to add some application logic to our project

This is part 8 of the series: How to create your own website based on Docker.

In the last part of the series, we have created our "dockerized" mongodb noSQL database server to read our persisted entries from and based on our architecture we have decided, that only the REST API (which will be based on ioJS) is allowed to talk to our database container.

So now it's about time to create the actual REST API that can be called via our nginx reverse proxy (using api.project-webdev.com) to read some person object entry from our database. We'll also create a very simple way to create a Person as well as list all available persons. As soon as you've understood how things work, you'll be able to implement more features of the REST API yourself - so consider this as pretty easy example.

Source code

All files mentioned in this series are available on Github, so you can play around with it! :)


Technologies to be used

Our REST API will use the following technologies:
  • ioJS as JavaScript application server
  • hapiJS as REST framework
  • mongoose as mongoDB driver, to connect to our database container
  • pm2 to run our nodejs application (and restart it if it crashes for some reason)

First things first - creating the ioJS image

Creating the ioJS image is basically the same every time. Let's create a new directory called /opt/docker/projectwebdev-api/ and within this new directory we'll create another directory called app and our Dockerfile:
# mkdir -p /opt/docker/projectwebdev-api/app/
# > /opt/docker/projectwebdev-api/Dockerfile
The new Dockerfile is based on the official ioJS Dockerfile, but I've added added some application/image specific information, so that we can implement our ioJS application:

  • Added our ubuntu base image (we're not using debian wheezy like in the official image)
  • Installed the latest NPM, PM2 and gulp (for later; we're not using gulp for this little demo)
  • Added our working directories
  • Added some clean up code
  • Added PM2 as CMD (we'll talk about that soon)

So just create your /opt/docker/projectwebdev-api/Dockerfile with the following content:
# Pull base image.
FROM docker_ubuntubase

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update
RUN apt-get update --fix-missing
RUN curl -sL https://deb.nodesource.com/setup_iojs_2.x | bash -

RUN apt-get install -y iojs gcc make build-essential openssl make node-gyp
RUN npm install -g npm@latest
RUN npm install -g gulp
RUN npm install -g pm2@latest
RUN apt-get update --fix-missing

RUN mkdir -p /var/log/pm2
RUN mkdir -p /var/www/html

# Cleanup
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN apt-get autoremove -y
RUN ln -s /usr/bin/nodejs /usr/local/bin/node

WORKDIR /var/www/html

CMD ["pm2", "start", "index.js","--name","projectwebdevapi","--log","/var/log/pm2/pm2.log","--watch","--no-daemon"]
Source: https://github.com/mastix/project-webdev-docker-demo/blob/master/projectwebdev-api/Dockerfile

Adding our REST API code to our container

Now let's create a simple application, that listens to a simple GET request and returns and entry from our mongoDB container. Just to proof that it works, I'll create a REST API that returns a simple Person object that contains an id as well as a first and a last name.

In order to get this object later, I'd have to call http://api.projectwebdev.com/person/{id} and it will return that object in JSON format. We'll also add a router to return all persons as well as a route that allows to add a new person - but we'll cover that in a second.

Since PM2 will only start (and not build) our ioJS application, we have to make sure that NPM (packaged with ioJS or nodeJS) is installed on your server, so that you can build the project there.

So here is my simple flow:

  • I create the ioJS application on my local machine
  • Then I upload the files to my server
  • On my server I use npm install to fetch all dependencies
  • PM2 restart the application automatically if it detects changes

In a later blog posting I will explain how you can setup a Git Push-To-Deploy mechanism which will take care of this automatically, but for this simple application we're doing it manually.

To get started, I'll create a new directory on my local machine (which has ioJS installed) and create a basic application:
# mkdir -p /home/mastixmc{development/projectwebdev-api && $_
# npm init
# npm install hapi mongoose --save
npm init will ask you a bunch of questions, and then write a package.json for you. It attempts to make reasonable guesses about what you want things to be set to, and then writes a package.json file with the options you've selected. (Info: Every nodeJS/ioJS application needs to have a package.json as descriptor)

npm install hapi mongoose --save will download/install hapiJS and mongoose and will save the dependency in our package.json file, so our server can download it later as well.

Creating the application

In our new directory, we'll create a file called index.js, with the following contents (we'll get into details afterwards):
var hapi = require('hapi');
var mongoose = require('mongoose');
// connect to database
mongoose.connect('mongodb://'+process.env.MONGODB_1_PORT_3333_TCP_ADDR+':'+process.env.MONGODB_1_PORT_3333_TCP_PORT+'/persons', function (error) {
    if (error) {
        console.log("Connecting to the database failed!");
        console.log(error);
    }
});
// Mongoose Schema definition
var PersonSchema = new mongoose.Schema({
    id: String,
    firstName: String,
    lastName: String
});
// Mongoose Model definition
var Person = mongoose.model('person', PersonSchema);
// Create a server with a host and port
var server = new hapi.Server();
server.connection({
    port: 3000
});
// Add the route to get a person by id.
server.route({
    method: 'GET',
    path:'/person/{id}',
    handler: PersonIdReplyHandler
});
// Add the route to get all persons.
server.route({
    method: 'GET',
    path:'/person',
    handler: PersonReplyHandler
});
// Add the route to add a new person.
server.route({
    method: 'POST',
    path:'/person',
    handler: PersonAddHandler
});
// Return all users in the database.
function PersonReplyHandler(request, reply){
    Person.find({}, function (err, docs) {
        reply(docs);
    });
}
// Return a certain user based on its id.
function PersonIdReplyHandler(request, reply){
    if (request.params.id) {
        Person.find({ id: request.params.id }, function (err, docs) {
            reply(docs);
        });
    }
}
// add new person to the database.
function PersonAddHandler(request, reply){
    var newPerson = new Person();
    newPerson.id = request.payload.id;
    newPerson.lastName = request.payload.lastname;
    newPerson.firstName = request.payload.firstname;
    newPerson.save(function (err) {
        if (!err) {
            reply(newPerson).created('/person/' + newPerson.id);    // HTTP 201
        } else {
            reply("ERROR SAVING NEW PERSON!!!"); // HTTP 403
        }
    });
}
// Start the server
server.start();
Disclaimer: Since this is just a little example, I hope you don't mind that I've put everything into on file - in a real project, I'd recommend to structure the project correctly, so that it scales in larger deployments - but for now, we're fine. Also, I did not add any error-checking or whatsoever to this code as it's just for demonstration purposes.

Now I we can copy our index.js and package.json file to our server (/opt/docker/projectwebdev-api/app/), ssh into our server and run npm install within that directory. This will download all dependencies and create a node_modules folder for us. You'll have a fully deployed ioJS application on your Docker host now, which can be used by the projectwebdev-api container, since this directory is mounted into it.

Explaining the REST-API code

So what does this file do? Pretty simple:

HapiJS creates a server that will listen on port 3000 - I've also added the following routes including their handlers:

  • GET to /person, which will then call a PersonReplyHandler function, that uses Mongoose to fetch all persons stored in our database.
  • GET to /person/{id}, which will then call a PersonIdReplyHandler function, that uses Mongoose to fetch a person with a certain id from our database.
  • POST to /person, which will then call a PersonAddHandler function, that uses Mongoose to store a person in our database.

A Person consists of the following fields (we're using the Mongoose Schema here):
// Mongoose Schema definition
var PersonSchema = new mongoose.Schema({
    id: String,
    firstname: String,
    lastname: String
});
So the aforementioned handlers (e.g. PersonAddHandler) will make sure that this information is served or stored from/to the database.

Later, when you have set up your nginx reverse proxy, you'll be able to use the following requests to GET or POST persons. But we'll get into that in the last part!

Add a new person:
curl -X POST -H "Accept: application/json" -H "Content-Type: multipart/form-data" -F "id=999" -F "firstname=Sascha" -F "lastname=Sambale" http://api.project-webdev.com/person
Result:
[{
    "_id": "555c827959a2234601c5ddfa",
    "firstName": "Sascha",
    "lastName": "Sambale",
    "id": "999",
    "__v": 0
}]
Get all persons:
curl -X GET -H "Accept: application/json" http://api.project-webdev.com/person/
Result:
[{
    _id: "555c81f559a2234601c5ddf9",
    firstName: "John",
    lastName: "Doe",
    id: "15",
    __v: 0
}, {
    _id: "555c827959a2234601c5ddfa",
    firstName: "Sascha",
    lastName: "Sambale",
    id: "999",
    __v: 0
}]
Get a person with id 999:
curl -X GET -H "Accept: application/json" http://api.project-webdev.com/person/999
Result:
[{
    "_id": "555c827959a2234601c5ddfa",
    "firstName": "Sascha",
    "lastName": "Sambale",
    "id": "999",
    "__v": 0
}]
You'll be able to do that as soon as you've reached the end of this series! ;)

Explaining the database code

I guess the most important part of the database code is how we establish the connection to our mongodb container.
// connect to database
mongoose.connect('mongodb://'+process.env.MONGODB_1_PORT_3333_TCP_ADDR+':'+process.env.MONGODB_1_PORT_3333_TCP_PORT+'/persons', function (error) {
    if (error) {
        console.log("Connecting to the database failed!");
        console.log(error);
    }
});
Since we're using container links, we can not know which ip our mongodb container will get when it gets started. So we have to use environment variables that Docker provides us.

Docker uses this prefix format to define three distinct environment variables:

  • The prefix_ADDR variable contains the IP Address from the URL, for example WEBDB_PORT_8080_TCP_ADDR=172.17.0.82.
  • The prefix_PORT variable contains just the port number from the URL for example WEBDB_PORT_8080_TCP_PORT=8080.
  • The prefix_PROTO variable contains just the protocol from the URL for example WEBDB_PORT_8080_TCP_PROTO=tcp.

If the container exposes multiple ports, an environment variable set is defined for each one. This means, for example, if a container exposes 4 ports that Docker creates 12 environment variables, 3 for each port.

In our case the environment variables look like this:

  • MONGODB_1_PORT_3333_TCP_ADDR
  • MONGODB_1_PORT_3333_TCP_PORT
  • MONGODB_1_PORT_3333_TCP_PROTO

Where MONGODB is the name and PORT is the port number we've specified in our docker-compose.yml file:
mongodb:
    build: ./mongodb
    expose:
      - "3333"

    volumes:
        - ./logs/:/var/log/mongodb/
        - ./mongodb/db:/data/db
Docker Compose also creates environment variables with the name DOCKER_MONGODB, which we are not going to use as it might happen that we switch from Docker Compose to something else in the future.

So Docker provides the environment variables and ioJS uses the process.env object to access them. We can therefore create a mongodb connection URL that looks like this:
mongodb://172.17.0.82:3333/persons
... which will be the link to our Docker container that runs mongodb on port 3333... Connection established!

Running ioJS in production mode

As mentioned before, in order to start (and automatically restart our REST API application, when we update the application files or the application crashes for some reason) we're using PM2, which will be configured via command line paramaters in our CMD instruction (see our Dockerfile):
CMD ["pm2", "start", "index.js","--name","projectwebdevapi","--log","/var/log/pm2/pm2.log","--watch","--no-daemon"]
So what does this command do?

  • "pm2", "start", "index.js" starts our application from within our WORKDIR (/var/www/html/).
  • "--name","projectwebdevapi" names our application projectwebdevapi.
  • "--log","/var/log/pm2/pm2-project.log" logs everything to /var/log/pm2/pm2-project.log (and since this is a mounted directory it will be stored on our docker host in /opt/docker/logs - see our docker-compose.yml file).
  • "--watch" watches our WORKDIR (/var/www/html/) for changes and will restart the application if something has changed. So you'll be able to update the application on your docker host and the changes will be reflected on the live site automatically.
  • "--no-daemon" runs PM2 in the foreground so the container does not exit and keeps running.

That's pretty much it - now, whenever you start your container later  (in our case Docker Compose will start it), PM2 will start your application and will make sure that it keeps running.

In the next part we'll create the frontend application that calls our new REST-API!

Friday, May 15, 2015

Series: How to create your own website based on Docker (Part 7 - Creating the mongodb Docker container)


Creating our mongodb database image

This is part 7 of the series: How to create your own website based on Docker.

It's about time to create our first image/container that is part of the real application stack. This container acts as persistence layer for the REST API (which we will create in the next part of this series). So the only component that talks to the database is the ioJS REST API container. In this part of the series, we'll have a look into how you can create your own mongodb container based on the official mongodb image.

Source code

All files mentioned in this series are available on Github, so you can play around with it! :)

Let's get started

Let's create a new directory called /opt/docker/mongodb/ and within this new directory we'll create two folders and one file:
# mkdir -p /opt/docker/mongodb/config/
# mkdir /opt/docker/mongodb/db/
# > /opt/docker/mongodb/Dockerfile
Since I don't want to re-invent the wheel, I'll have a look at the official mongodb Docker image and we're basically using the same mongodb 3.0 Dockerfile for our design. Since we want to run this mongodb database on our own Ubuntu Base Container, we need to make some changes to official mongodb Docker image.

The official mongodb Dockerfile uses Debian wheezy as base image, which is not what we want:
FROM debian:wheezy
We are going to use our own Ubuntu Base Image for the mongodb image and since we use Docker Compose, we must specify the correct base image name, which is a concatenation of "docker_" and the image name that we have specified in our docker-compose.yml - so in our case that would be "docker_ubuntubase". So we're changing the aforementioned line to use our base image:
# Pull base image.
FROM docker_ubuntubase 
Since the original Dockerfile only allows us to mount /data/db as volume, so we're extending it to also allow the mongodb log directory:

Replace the following line:
VOLUME /data/db
With this line:
VOLUME ["/data/db","/var/log/mongodb/"]
I'd like to have my configurations in a subfolder called "config", so we need to adjust another line:

Replace the following lines:
COPY docker-entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
With these lines:
COPY ./config/docker-entrypoint.sh /tmp/entrypoint.sh
RUN ["chmod", "+x", "/tmp/entrypoint.sh"]
ENTRYPOINT ["/tmp/entrypoint.sh"]
These lines copy a script called ./config/docker-entrypoint.sh to the /tmp/ folder in the container, make it executable and run it once, once the container has started. You can find the docker-entrypoint.sh file in the official mongodb docker repository on GitHub. Just copy that file into the config directory, which you have to create if you haven't done so already.

Let's create our own mongodb configuration file to set some parameters.

To do so, create a file called /opt/docker/mongodb/config/mongodb.conf and add the following lines (important: YAML does not accept tabs; use spaces instead!):
systemLog:
   destination: file
   path: "/var/log/mongodb/mongodb-projectwebdev.log"
   logAppend: true
storage:
   journal:
      enabled: true
net:
   port: 3333
   http:
       enabled: false
       JSONPEnabled: false
       RESTInterfaceEnabled: false
Now add the following lines to your Dockerfile to copy our new custom config file to our image:
RUN mkdir -p /var/log/mongodb && chown -R mongodb:mongodb /var/log/mongodb
COPY ./config/mongodb.conf /etc/mongod.conf
Since we want to load our custom config now, we'll need to changed the way we start mongodb, so we change the following line from
CMD ["mongod"]
to
CMD ["mongod", "-f", "/etc/mongod.conf"]
Our folder structure must look like this now:
+-- /opt/docker/mongodb
¦   +-- config
¦   ¦   +-- docker-entrypoint.sh
¦   ¦   +-- mongodb.conf
¦   +-- db
¦   +-- Dockerfile
Another thing we can remove is the EXPOSE instruction, since we already specified that in our docker-compose.yml.

So the complete Dockerfile will look like this now:
# Pull base image.
FROM docker_ubuntubase

# add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added
RUN groupadd -r mongodb && useradd -r -g mongodb mongodb

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates curl \
numactl \
&& rm -rf /var/lib/apt/lists/*

# grab gosu for easy step-down from root
RUN gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4
RUN curl -o /usr/local/bin/gosu -SL "https://github.com/tianon/gosu/releases/download/1.2/gosu-$(dpkg --print-architecture)" \
&& curl -o /usr/local/bin/gosu.asc -SL "https://github.com/tianon/gosu/releases/download/1.2/gosu-$(dpkg --print-architecture).asc" \
&& gpg --verify /usr/local/bin/gosu.asc \
&& rm /usr/local/bin/gosu.asc \
&& chmod +x /usr/local/bin/gosu

# gpg: key 7F0CEB10: public key "Richard Kreuter <richard@10gen.com>" imported
RUN apt-key adv --keyserver ha.pool.sks-keyservers.net --recv-keys 492EAFE8CD016A07919F1D2B9ECBEC467F0CEB10

ENV MONGO_MAJOR 3.0
ENV MONGO_VERSION 3.0.3

RUN echo "deb http://repo.mongodb.org/apt/debian wheezy/mongodb-org/$MONGO_MAJOR main" > /etc/apt/sources.list.d/mongodb-org.list

RUN set -x \
&& apt-get update \
&& apt-get install -y mongodb-org=$MONGO_VERSION \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /var/lib/mongodb \
&& mv /etc/mongod.conf /etc/mongod.conf.orig

RUN mkdir -p /data/db && chown -R mongodb:mongodb /data/db
RUN mkdir -p /var/log/mongodb && chown -R mongodb:mongodb /var/log/mongodb

VOLUME ["/data/db","/var/log/mongodb/"]

COPY ./config/docker-entrypoint.sh /tmp/entrypoint.sh
COPY ./config/mongodb.conf /etc/mongod.conf
RUN ["chmod", "+x", "/tmp/entrypoint.sh"]

ENTRYPOINT ["/tmp/entrypoint.sh"]

CMD ["mongod", "-f", "/etc/mongod.conf"]
Source: https://github.com/mastix/project-webdev-docker-demo/blob/master/mongodb/Dockerfile

This is pretty much it. That's all we need to create our mongodb database container, that will run on the default port 3333 - but will only be accessible from the REST API, since we linked that container to the ioJS REST API container only, see our docker-compose.yml file again:
projectwebdevapi:
    build: ./projectwebdev-api
    expose:
        - "3000"
    links:
        - mongodb:db

    volumes:
        - ./logs/:/var/log/supervisor/
        - ./projectwebdev-api/app:/var/www/html
In the next chapter it's getting more interesting: Let's create our REST API container, that talks to the mongodb container!

Series: How to create your own website based on Docker (Part 6 - Creating Ubuntu Base Image)

Creating our Ubuntu Base Image

This is part 6 of the series: How to create your own website based on Docker.

Before we can run a Docker container, we need to create a Docker image. So why is that? Well, Docker images are read-only templates to create Docker containers and these images define everything Docker needs to know to create containers (you can run several containers based on the same image).

In this part of the series, we're going to create the base image for all our other images (as they should all run on Ubuntu). This image will be a basic Ubuntu image that brings all the software/tools/drivers that is going to be needed by all images depending on it - that's why I'm calling it the "base image".

Source code

All files mentioned in this series are available on Github, so you can play around with it! :)

Using the official Ubuntu Base Image

There are two cool things that Docker provides and we will make use of:
  • You can use the Docker Hub to get already created images (you can also upload your image to Docker Hub, but we're not doing it here).
  • Whatever we install/set up in our Ubuntu image,can be reused in all other containers that are built on this base image - so these settings are shared among all other containers that are based on that image.
Important notice: When running servers in production I do always recommend to use official images as base image, since you can't be sure if some bad guy added some malicious stuff to that image and let's be honest, it's much cooler to create the image yourself! To get a list of available (official) images, just visit the Docker Hub Registry.

For our projectwebdev website we're using the official Ubuntu image and will add our custom software and settings. You will see that the steps we're doing now are pretty much the same every time, so I'll write them down more generic first, before we get into details:
  1. Create a directory within your Docker directory (in our case that would be /opt/docker/ubuntu-base).
  2. Create a Dockerfile (the template that describes the image) within that new directory (so the file would be /opt/docker/ubuntu-base/Dockerfile).
  3. Create additional directories that contain either scripts or config files that should be added to the image/container.
  4. Create additional directories to act as volume mounts for the containers.
As mentioned before, we're going to use the official image and will then install the software that will be needed by all containers that are based on that image. So let's create our Dockerfile (/opt/docker/ubuntu-base/Dockerfile) by adding the image to pull from the Docker Hub.

Get the base image

If we want to use the latest available Ubuntu distribution, we can type something like this:
# Pull base image.
FROM ubuntu:latest
In our case that would be Ubuntu 15.04 (Vivid Vervet), but I'll always recommend to specify the version number explicitly so we don't get surprised with a new Ubuntu version, as soon as we build our images after October when Ubuntu 15.10 (Wily Werewolf) is released.
# Pull base image.
FROM ubuntu:15.04
As you can see here, there are more versions of Ubuntu available, so you can also pull Ubuntu 14.04 LTS (Trusty Thar) from the hub.

Install all needed packages

Since we're working with several technologies later, it's good to have a solid software base under the hood. That's why I've installed the following packages in my base image, just to make sure they are available:
  • build-essential
  • curl
  • git
  • man
  • software-properties-common
  • unzip
  • vim
  • wget
I do always run apt-get update and apt-get dist-upgrade every time I build my images, to make sure that I'm on the latest packages, whenever I rebuild everything - I also clean up some space by deleting all unnecessary package stuff.

So the second part of our Dockerfile looks like this:
# Install the software/packages needed for all other containers
RUN \
  sed -i 's/# \(.*multiverse$\)/\1/g' /etc/apt/sources.list && \
  apt-get update && \
  apt-get -y upgrade && \
  apt-get install -y build-essential software-properties-common curl git man unzip vim wget && \
  rm -rf /var/lib/apt/lists/*

Setup some variables and run

The next step is to set an environment variable (ENV) and a WORKDIR (sets the working directory for the following CMD instruction).
# Set environment variables.
ENV HOME /root 
# Define working directory.
WORKDIR /root
No we're telling our image what command to run when the container is started - in our case that would be bash:
# Define default command.
CMD ["bash"]
That's it - that's our Dockerfile - so here it is completely:
# Pull base image.
FROM ubuntu:15.04
# Install the software needed for all other containers
RUN \
  sed -i 's/# \(.*multiverse$\)/\1/g' /etc/apt/sources.list && \
  apt-get update && \
  apt-get -y upgrade && \
  apt-get install -y build-essential software-properties-common curl git man unzip vim wget && \
  rm -rf /var/lib/apt/lists/*
# Set environment variables.
ENV HOME /root
# Define working directory.
WORKDIR /root
# Define default command.
CMD ["bash"]
Source: https://github.com/mastix/project-webdev-docker-demo/blob/master/ubuntu-base/Dockerfile

Testing the Ubuntu Base Image/Container without Docker Compose

You can basically test you new container without using Docker Compose. First build you new image like this:
# cd /opt/docker/ubuntu-base
# docker build -t projectwebdev/ubuntu-base .
Result:
Sending build context to Docker daemon 3.584 kB
Sending build context to Docker daemon
Step 0 : FROM ubuntu:15.04
15.04: Pulling from ubuntu
b68f8c8d2140: Pull complete
1d57666667e5: Pull complete
a216ec781532: Pull complete
bd94ae587483: Already exists
ubuntu:15.04: The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security.
Digest: sha256:6994d0f1e915ff22a9b77433c19ce619eda61e5a431a7ba89230327b2f289a95
Status: Downloaded newer image for ubuntu:15.04
 ---> bd94ae587483
Step 1 : RUN sed -i 's/# \(.*multiverse$\)/\1/g' /etc/apt/sources.list &&   apt-get update &&   apt-get -y upgrade &&   apt-get install -y build-essential &&   apt-get install -y software-properties-common &&   apt-get install -y byobu curl git htop man unzip vim wget &&   rm -rf /var/lib/apt/lists/*
 ---> Running in 2d49adfb4fce
Ign http://archive.ubuntu.com vivid InRelease
Ign http://archive.ubuntu.com vivid-updates InRelease
[...]
Step 2 : ENV HOME /root
 ---> Running in 1923ed0e21a0
 ---> a5b574f11a0f
Removing intermediate container 1923ed0e21a0
Step 3 : WORKDIR /root
 ---> Running in 4bfbeeea1733
 ---> dff8fc1d0b06
Removing intermediate container 4bfbeeea1733
Step 4 : CMD bash
 ---> Running in 6b086dd626ac
 ---> cfad3f94c992
Removing intermediate container 6b086dd626ac
Successfully built cfad3f94c992
Now we can run our container and connect to bash (remember that we have set bash as CMD in our Dockerfile):
# docker run --name ubuntu-base-test -t -i projectwebdev/ubuntu-base
Here's the result - we've been able to trigger commands withing our new Ubuntu container - I've also called env to show you the environment variable HOME that we have set:
# docker run --name ubuntu-base-test -t -i projectwebdev/ubuntu-base
root@06913990a3c8:~# env
HOSTNAME=06913990a3c8
[...]
HOME=/root
[...]

Cleanup image file and docker container

Since this was just a basic test, we'll have to remove the image and the container to save some space.
# docker rm -f ubuntu-base-test
# docker rmi -f projectwebdev/ubuntu-base
That's it! Our Ubuntu base image is up and running and can therefore be started via Docker Compose. Let's create the mongodb container in the next part!