Andy Maes – 4 October 2016
1239 words in about 5 minutes

Introduction

“Our workshop is a next step for you to gain some hands-on experience”. This is the subject of the Docker-workshop we did at Kabisa on Wednesday October 5th. We did a workshop for people who read about docker but wanted to get some hands-on experience. Because it can be hard to get started we did a demo to present the basics. This blog-post is a write-up of that demo. As our “test subject” we will use Ghost — a lean and mean blogging application using NodeJS on the backend and EmberJS on the frontend.

docker-workshop social image

Part 1: Basic docker commands

First thing we need is an image. We can use an image as the base for our own image or turn it into a container by running it with the docker run command. Do we have images on our machine? We can check this by running the following command:

1
docker images
An image is a snapshot of a container

How do we get an image on our local machine so we can run it?

Well… we can pull it:

1
docker pull ghost

Check if it is there by listing all images:

1
docker images

We can run it now:

1
docker run --name myawesomeblog -P -d ghost

It will return the container_id because -d means, run the container in detached mode. We can verify that our awesome blog is running with the command

1
2
docker ps
docker ps -a    # All containers (also stopped)

and check which port on the host we need to browse to. We don’t know this because we passed -P into the run command. This will map every exposed port on the container to a high and free port on the host. If we visit localhost:[port] we will see our awesomeblog.


Part 2: Running with Mysql

In Docker we always try to run one process in each container. That is why we are going to setup a MySQL container. Let’s visit Docker Hub again and search for mysql. The documentation shows we need to provide a couple of options when running MySQL.

Let’s see if we can run MySQL. We don’t need to pull it first, it pulls when it’s not available locally.

1
2
3
4
5
6
7
docker run --name mysql \
    -e MYSQL_ROOT_PASSWORD=<root_pass> \
    -e MYSQL_USER=<user_name> \
    -e MYSQL_PASSWORD=<user_pass> \
    -v=/Users/<user>/.DATA/MYSQL/5.7.15:/var/lib/mysql \
    -p 3306:3306 \
    -d mysql:5.7.15

Now we are able to connect to this container with the credentials we passed into the run command. The -p option tells docker to map port 3306 of the host to 3306 of the container. So when we put the following command on the commandline, we can manage our databases.

1
2
mysql -u root -p -h 127.0.0.1
Enter password:

Be aware that Ghost (and maybe your app too) needs an existing DB and User. The MySQL user needs privileges to access the DB from another Host (container).

1
2
3
4
mysql> create database ghost_production;
mysql> CREATE USER '<user>'@'localhost' IDENTIFIED BY '<password>';
mysql> GRANT ALL PRIVILEGES ON ghost_production.* TO '<user>'@'%';
mysql> FLUSH PRIVILEGES;

Part 3: Building ghost with custom configuration

As we saw earlier the base Ghost image uses sqlite. If we want our container to use the MySQL container we need to:

We could start with a Dockerfile.

A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image.

But this also means we could start the base image and do these steps manually. Let’s give that a shot first.

We will use the –link option to tell our container to add a “network connection” to the mysql container and give it an alias inside this container of db.

Because our new config only consists of the production part, we need to pass in the environment variable NODE_ENV=production.

We create a new configuration file for Ghost (you can find config.js below) and we mount it inside the container.

config.production.js: ˜˜˜ js var path = require(‘path’), config;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
config = {
  production: {
    url: 'http://blogaboutit.nl',
    mail: {},
    database: {
        client: 'mysql',
        connection: {
            host: 'db',
            user: '<mysql user>',
            password : '<mysql password>',
            database : 'ghost_production',
            port: '3306',
            charset  : 'utf8'
        },
        debug: false
    },
    paths: {
        contentPath: path.join(process.env.GHOST_CONTENT, '/')
    },
    server: {
        host: '0.0.0.0',
        port: '2368'
    }
  }
};

module.exports = config; ˜˜˜ Let's run it...

docker run -d -P --link="mysql:db" -e "NODE_ENV=production" -v="$(pwd)/config.production.js:/var/lib/ghost/config.js" --name myblog ghost

To check if this really works see if the application created the tables inside the DB of your MySQL container.


Part 4: Running with docker-compose and nginx

The app container

First we will describe the manual steps we took for running the Ghost blog with MySQL in a new Dockerfile so we can always rebuild our container. We keep it as simple as possible with this Dockerfile:

1
2
3
4
5
FROM ghost
COPY ./config.production.js /var/lib/ghost/config.js
ENTRYPOINT ["/entrypoint.sh"]
EXPOSE 2368
CMD ["npm", "start", "--production"]

The tasks called in this file are, getting the Ghost base-image and copy our local config file to the correct location inside the container (i found the original file inside the base image first). It will overwrite the existing file. The rest is copied from the base image Dockerfile, except for CMD, which now runs our app in production-mode.

The Nginx container

To create a proper Nginx container we create the following Dockerfile

1
2
3
4
5
6
7
8
9
10
FROM nginx
COPY blogaboutit.conf /etc/nginx/sites-available/blogaboutit.conf
COPY nginx.conf /etc/nginx/nginx.conf

RUN mkdir -p /etc/nginx/sites-enabled && \
    ln -s /etc/nginx/sites-available/blogaboutit.conf /etc/nginx/sites-enabled/blogaboutit.conf && \
    rm -f /etc/nginx/sites-enabled/default

EXPOSE 80
CMD ["nginx", "-c", "nginx.conf", "-g", "daemon off;"]

In the Dockerfile above, we copy two files into the container. Let’s have a look at these.

blogaboutit.conf Virtual host config found in the Ghost documentation

1
2
3
4
5
6
7
8
9
10
server {
  listen 80;
  server_name blogaboutit.nl;

  location / {
      proxy_set_header   X-Real-IP $remote_addr;
      proxy_set_header   Host      $http_host;
      proxy_pass         http://app:2368;
  }
}

nginx.conf Added only one line to the config inside the container, to enable sited-enabled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Start Nginx from inside the directory with the nginx Dockerfile and both configuration files.

1
docker build -t <dockerhub-user>/blogaboutit-nginx .

Docker-compose

Docker-compose, composes the different components of your application together into one working application. It handles building, running and connecting (networking) of containers.

What does our compose file look like?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
version: '2'

networks:
  blogaboutit:

services:
  app:
    image: <dockerhub-user>/blogaboutit
    build: .
    container_name: blogaboutit-app
    networks:
      - blogaboutit
    environment:
      NODE_ENV: production
    depends_on:
      - db
    ports:
      - "2368:2368"
    restart: always
  
  nginx:
    image: <dockerhub-user>/blogaboutit-nginx
    container_name: blogaboutit-nginx
    networks:
      - blogaboutit
    ports:
      - "80:80"
    depends_on:
      - app
    restart: always

  db:
    image: "mysql:5.7.15"
    container_name: blogaboutit-mysql
    networks:
      - blogaboutit
    environment:
      MYSQL_ROOT_PASSWORD: <root_pass>
      MYSQL_USER: <user>
      MYSQL_PASSWORD: <user_pass>
    ports:
      - "3306:3306"
    volumes:
      - /Users/<user>/.DATA/MYSQL/5.7.15:/var/lib/mysql
    restart: always    

Now run docker-compose up -d

For more info please have a look at the Docker documentation.

Online Docker Workshop

We’re big fans of Docker and we regularly like to share our knowledge through workshops or hackathons. They’re free and everyone is welcome!

If you’d like to join us for our next online workshop, let us know and we’ll send you an invite when the next one is.