The beauty of Docker - how to run all Butler tools with a single command

The beauty of Docker - how to run all Butler tools with a single command
Photo by Ian Taylor / Unsplash

Docker is great.

Docker is one of those tools that have the potential to fundamentally transform how you develop and run software – once you have tried Docker it is hard to imagine going back to something else.

In previous posts we have seen how Butler, Butler SOS and Butler CW can be run as Docker containers.
But we can do even better – why not control all the Butler tools from a single docker-compose file? Maybe even specifying the dependencies on influxdb and mqtt in there too?

Setting this up is incredibly easy – a single docker-compose file tells Docker what containers to use, and some config files tells the Butler tools where to find things.

Let’s get started!

Setting things up

Docker containers can be started and managed in different ways. We will use the docker-compose command to create and start the containers, as it uses an use easy to understand YAML syntax to define what containers to create, and what parameters to send to them.

The scaffolding

First, let’s create some directories where we can store configuration files for the different Butler tools.

If you just want to try this out, these directories can be on your local computer.

If doing this in a real Qlik Sense environment, you should of course place these directories on a suitable server. The interesting thing is that you have a lot of freedom here: if you have an existing Docker infrastructure the Butler tools can happily run there.

In the examples below I have used my regular 3-year old Apple laptop that sits on the same network as the Qlik Sense server.

Directories

Let’s create some directories first:

proton:code goran$ mkdir butler-docker
proton:code goran$ cd butler-docker/
proton:butler-docker goran$ mkdir certificate config_butler config_butler-cw config_butler-sos log_butler-cw
proton:butler-docker goran$ tree
.
├── certificate
├── config_butler
├── config_butler-cw
├── config_butler-sos
└── log_butler-cw

5 directories, 0 files
proton:butler-docker goran$

Next, copy the config files from each Butler tool into their respective directories above. Also place the certificates exported from the QMC in the certificate directory.

Now we have something like this:

proton:butler-docker goran$ tree
.
├── certificate
│   ├── client.pem
│   ├── client_key.pem
│   └── root.pem
├── config_butler
│   └── production.yaml
├── config_butler-cw
│   ├── apps.yaml
│   └── production.yaml
├── config_butler-sos
│   └── production.yaml
└── log_butler-cw

5 directories, 7 files
proton:butler-docker goran$

Config files

The YAML config files above are the same ones that were used in the various standalone Butler tools (see previous posts [1], [2], [3]). This is the beauty of Docker – the container contains everything that the software inside needs, and the configuration is the same no matter if used on a laptop, on  a server or in the cloud.

The docker-compose file

The docker-compose.yml file is where we tell Docker what containers should be created and how to configure them.

The file should be created in the main directory (one level up from the config directories) and is basically just a combination of the docker-compose files used in previous blog posts:

proton:butler-docker goran$ cat docker-compose.yml
# docker-compose.yml
version: '2.2'
services:
  butler-sos:
    image: ptarmiganlabs/butler-sos:latest
    init: true
    container_name: butler-sos
    restart: always
    volumes:
      # Make config file accessible outside of container
      - "./config_butler-sos:/nodeapp/config"
      - "./certificate:/nodeapp/config/certificate"
    environment:
      - "NODE_ENV=production"
    logging:
      driver: json-file

  butler-cw:
    image: ptarmiganlabs/butler-cw:latest
    init: true
    container_name: butler-cw
    restart: always
    volumes:
      # Make config file accessible outside of container
      - "./config_butler-cw:/nodeapp/config"
      - "./log_butler-cw:/nodeapp/log"
      - "./certificate:/nodeapp/config/certificate"
    environment:
      - "NODE_ENV=production"
    logging:
      driver: json-file

  butler:
    image: ptarmiganlabs/butler:latest
    container_name: butler
    restart: always
    ports:
      - "8180:8080" # REST API available on port 8180 to services outside the container
      - "9997:9997" # UDP port for session connection events
      - "9998:9998" # UDP port for task failure events
    volumes:
      # Make config file accessible outside of container
      - "./config_butler:/nodeapp/config"
      - "./certificate:/nodeapp/config/certificate"
    environment:
      - "NODE_ENV=production"
    logging:
      driver: json-file
proton:butler-docker goran$

The only difference compared to the docker-compose.yml file used for running the tools stand-alone is how the certificate folder is mapped.

In the file above there is an extra volume mapping for each service, this is just so we get a single/shared certificate directory where the QMC certificates can be placed.

Start things up

Now comes the magic. A single command, and everything starts.

It’s even better than that, as Docker will also pull the latest versions of the Butler images from Docker Hub. No need to manually download anything.

Docker then creates containers based on these images and finally start the containers.

The whole thing looks like this:

Butler tools running as Docker containers

That’s pretty neat…

Adding MQTT, Influxdb and Grafana

We can do even better though.

Let’s also start some other tools from the docker-compose file:

  • Mosquitto (which is a very good MQTT broker used by most of the Butler tools)
  • Influxdb (time series database used by Butler SOS)
  • Grafana (real-time charts used by Butler SOS)

The docker.compose.yml file now looks like this:

# docker-compose.yml
version: '2.2'
services:
  butler-sos:
    image: ptarmiganlabs/butler-sos:latest
    depends_on:
      - mqtt
      - influxdb
      - grafana
    init: true
    container_name: butler-sos
    restart: always
    volumes:
      # Make config file accessible outside of container
      - "./config_butler-sos:/nodeapp/config"
      - "./certificate:/nodeapp/config/certificate"
    environment:
      - "NODE_ENV=production"
    networks:
      - senseops
    logging:
      driver: json-file

  butler-cw:
    image: ptarmiganlabs/butler-cw:latest
    init: true
    container_name: butler-cw
    restart: always
    volumes:
      # Make config file accessible outside of container
      - "./config_butler-cw:/nodeapp/config"
      - "./log_butler-cw:/nodeapp/log"
      - "./certificate:/nodeapp/config/certificate"
    environment:
      - "NODE_ENV=production"
    networks:
      - senseops
    logging:
      driver: json-file

  butler:
    image: ptarmiganlabs/butler:latest
    depends_on:
      - mqtt
    container_name: butler
    restart: always
    ports:
      - "8180:8080" # REST API available on port 8180 to services outside the container
      - "9997:9997" # UDP port for session connection events
      - "9998:9998" # UDP port for task failure events
    volumes:
      # Make config file accessible outside of container
      - "./config_butler:/nodeapp/config"
      - "./certificate:/nodeapp/config/certificate"
    environment:
      - "NODE_ENV=production"
    networks:
      - senseops
    logging:
      driver: json-file

  mqtt:
    image: eclipse-mosquitto:latest
    container_name: mosquitto
    restart: always
    ports:
      - "1884:1883"
      - "9002:9001"
    volumes:
      - "./mosquitto/data:/mosquitto/data"
      - "./mosquitto/config:/mosquitto/config"
      - "./mosquitto/log:/mosquitto/log"
    networks:
      - senseops
    logging:
      driver: json-file

  influxdb:
    image: influxdb:latest
    container_name: influxdb
    restart: always
    ports:
      - "8086:8086"
    volumes:
      - "./influxdb/datastore:/var/lib/influxdb"
    networks:
      - senseops
    logging:
      driver: json-file

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: always
    ports:
      - "3001:3000"
    volumes:
      - "./grafana/datastore:/var/lib/grafana"
    networks:
      - senseops
    logging:
      driver: json-file

networks:
  senseops:
    driver: bridge

Note that the config files for the different Butler tools must also be modified to point to the local instances of Mosquitto, Influxdb and Grafana.
For example, the Butler SOS production.yaml file should now use your local computer’s IP as the host IP for Influxdb, MQTT and Grafana.

The complete directory structure needed for the docker-compose file above looks like this:

proton:butler-docker goran$ tree
.
├── certificate
│   ├── client.pem
│   ├── client_key.pem
│   └── root.pem
├── config_butler
│   ├── certificate
│   └── production.yaml
├── config_butler-cw
│   ├── apps.yaml
│   ├── certificate
│   └── production.yaml
├── config_butler-sos
│   ├── certificate
│   └── production.yaml
├── docker-compose.yml
├── grafana
│   └── datastore
├── influxdb
│   └── datastore
├── log_butler-cw
│   ├── debug.log
│   ├── error.log
│   ├── info.log
│   └── verbose.log
└── mosquitto
    ├── config
    ├── data
    └── log

16 directories, 12 files
proton:butler-docker goran$

Running “docker-compose up” starts all 6 services. Nice.

Note how dependencies are defined, making Influxdb, Mosquitto and Grafana start first.

We now have a complete SenseOps environment running locally on our own computer, while at the same time interacting with one or more Qlik Sense servers on the network. This is a really good way to try out the various Butler tools without having to install any software on the servers.

Wrapping up

The mini series of blog posts about running Butler tools as Docker containers is now complete.

There is one tool – the Butler App Duplicator – that has not been discussed so far. There is no reason why this tool cannot be run from a Docker container too, it just hasn’t been a focus so far. Maybe that will change in the future – or you are very welcome to fork the Github repository and contribute to the code!