Orchestrate Multiple Docker Containers Simply Using Fig

0
2103
6 min read

When you start learning how to use Docker, you play around running a single container in a single project. Soon after, you want to start multiple Docker containers in multiple projects.

A nifty little tool called Fig helps you do just that.

Sneak preview

At the end of this blog post, you will have the following small sample app running on Docker using a MongoDB database and a Node.js web server in two separate containers:

 


But first, some background.

Bash vs Fig

Before this article, I had written my own shell script for the setup of named-data.education. But as I was exploring the realm of Docker orchestration, I came to the conclusion that throwing my script out and going with an existing solution would be the better practice.

I ended up choosing Fig because it supports (and simplifies) the workflow I had implemented with my custom shell script. Also, it was recently acquired by Docker, and will soon be integrated with Docker.

What does Fig do for you?

  • It builds, runs, and removes multiple containers together in a single command.
  • It keeps docker command-line arguments out of sight and inside the fig.yml file. This reduces long docker run … commands to a simple fig up command, similar to vagrant up.
  • It avoids naming conflicts by giving image and container names project-specific prefixes, derived from the name of the directory that contains the fig.yml file.
  • It knows the state in which the application environment is in, so docker ps ; docker stop ; docker rm just becomes fig up, which restarts running containers transparently.
  • The fig.yml file is much more readable and maintainable than an equivalent shell script would be.

Life cycle of a single Docker container (without Fig)

This state diagram illustrates the life cycle of a typical Docker container:

 

States

  • source – There is a Dockerfile, but nothing was done with it
  • built – A Docker image was built from the Dockerfile
  • running – A Docker container was started from the Docker image
  • stopped – The Docker container has been stopped or it has stopped on its own

Transitions

These actually correspond to Docker commands, the most prominent ones being build and run:

  • build – Takes a Dockerfile and creates a Docker image
  • run – Takes a Docker image and runs a command in a container
  • stop – Stops a running Docker container, or the container dies
  • rm – Removes a stopped Docker container
  • rmi – Removes a Docker image

Life cycle of multiple Docker containers with Fig

This diagram illustrates how Fig orchestrates multiple Docker containers:

These transitions also correspond to Fig commands, with fig up being the champion here. (There is also a fig run command, but it has a marginal role in comparison.)

Okay, let’s get practical.

A web app and a database

As an example, let’s say your app is really simple and consists of just a database and a web frontend:

In Docker, this architecture is implemented by running each part in a separate container. The external connection, between NodeJS and the Internet, is realized by exposing a port, whereas the internal connection, between NodeJS and MongoDB, is realized using a link.

Application source code

For this sample application, create a directory with the following layout with the listings shown below, or get the source code from GitHub:

fig-nodejs-mongodb-example/
 fig.yml
 web/
   Dockerfile
   liststorage.coffee
   package.json
   server.coffee

fig.yml:

web:
 build: ./web
 ports:
   - "8080:8080"
 links:
   - db
 
db:
 image: mongo:2.6

Dockerfile:

FROM node:0.10
 
ADD package.json /code/
WORKDIR /code
RUN npm install
ADD . /code
 
CMD ["./node_modules/.bin/coffee", "./server.coffee"]

liststorage.coffee:

{MongoClient, ObjectID} = require 'mongodb'
 
class module.exports.ListStorage
 constructor: ->
   @ready = false
   @collection = null
   MongoClient.connect 'mongodb://db_1:27017/list', (err, db) =>
     throw err if err
     db.createCollection 'list', (err, collection) =>
       throw err if err
       @ready = true
       @collection = collection
 
 toArray: (callback) ->
   return callback new Error 'not ready' unless @ready
   @collection.find().toArray (err, list) ->
     return callback err if err
     callback null, list
 
 push: (item, callback) ->
   doc = item: item
   @collection.insert doc, {w: 1}, (err, result) ->
     return callback err if err
     callback null
 
 remove: (_id, callback) ->
   @collection.remove {_id: ObjectID(_id)}, {w: 1}, (err, result) ->
     return callback err if err
     callback null

package.json: (The bare minimum is to make npm install happy, but normally npm init should be used to create this file.)

{
 "dependencies": {
   "body-parser": "^1.6.6",
   "coffee-script": "^1.8.0",
   "express": "^4.8.5",
   "handlebars": "^2.0.0-beta.1",
   "mongodb": "^1.4.9"
 }
}

server.coffee:

#!/usr/bin/env coffee
 
require 'coffee-script/register'
ListStorage = require('./liststorage').ListStorage
listStorage = new ListStorage
 
handlebars = require 'handlebars'
 
indexTemplate = handlebars.compile '''
 <title>List</title>
 <h1>List</h1>
 <ul>
   {{#each items}}
     <li><a href="/delete/{{_id}}">[&times;]</a> {{item}}</li>
   {{/each}}
 </ul>
 <form method="POST">
   <label>Add something:</label>
   <input name="item" autofocus="autofocus" />
   <input type="submit" value="Submit" />
 </form>
'''
 
express = require 'express'
bodyParser = require 'body-parser'
app = express()
 
app.use bodyParser.urlencoded extended: true
 
app.get '/', (req, res) ->
 listStorage.toArray (err, items) ->
   throw err if err
   res.send indexTemplate items: items
 
app.post '/', (req, res) ->
 listStorage.push req.body.item, (err) ->
   throw err if err
   res.redirect '/'
 
app.get '/delete/:_id', (req, res) ->
 listStorage.remove req.params._id, (err) ->
   throw err if err
   res.redirect '/'
 
app.listen 8080

Start it up

First, make sure you have Docker and Fig installed. (This example has been tested with Fig 0.5.2 and Docker 1.2.0. On OS X, brew install fig works fairly well, together with docker-osx or boot2docker.)

Then, open the terminal and type the following:

cd fig-nodejs-mongodb-example
fig up -d

Then (assuming you run docker-osx), open http://localdocker:8080/ and play around – knowing that you did not have to manually set up two virtual machines!

Other commands you might wish to use

  • fig up (Ctrl-C will stop this from running)
  • fig logs – for logging
  • fig stop – for stopping
  • fig rm – for removing
  • fig ps – for status

Remarks

The sections in fig.yml are called “services.”

As Fig allows scaling by running multiple instances of services, link aliases get an additional suffix inside containers compared to plain Docker; db_1 for Fig versus just db for Docker.

Also, as Docker (and Fig) manage the container’s /etc/hosts file, you get a db_1 host for free.

Now, go have fun and keep containin’!

About the author

Felix Rabe has been programming and working with different technologies and companies at different levels since 1993. Currently, he is researching and promoting Named Data Networking (http://named-data.net/), an evolution of the Internet architecture that currently relies on the host-bound Internet Protocol.

LEAVE A REPLY

Please enter your comment!
Please enter your name here