Compose for development stacks

Compose for development stacks

Dockerfiles are great to build container images.

But what if we work with a complex stack made of multiple containers ?

Eventually, we will want to write some custom scripts and automation to build, run, and connect our containers together.

There is a better way: using Docker Compose.

In this section, you will use Compose to bootstrap a development environment

What is Docker Compose ?

Docker Compose (formerly known as fig) is an external tool.

Unlike the Docker Engine, it is written in Python. It’s open source as well.

The general idea of Compose is to enable a very simple, powerful onboarding workflow:

  • Checkout your code.

  • Run docker-compose up.

  • Your app is up and running !

Compose overview

This is how you work with Compose:

  • You describe a set (or stack) of containers in a YAML file called docker-compose.yml.

  • You run docker-compose up.

  • Compose automatically pulls images, builds containers, and starts them.

  • Compose can set up links, volumes, and other Docker options for you.

  • Compose can run the containers in the background, or in the foreground.

  • When containers are running in the foreground, their aggregated output is shown.

Before diving in, let’s see a small example of Compose in action.

Checking if Compose is installed

If you are using the official training virtual machines, Compose has been pre-installed.

You can always check that it is installed by running:

$ docker-compose --version

Launching Our First Stack with Compose

First step: clone the source code for the app we will be working on.

$ cd
$ git clone git://github.com/jpetazzo/trainingwheels
...
$ cd trainingwheels

Second step: start your app.

$ docker-compose up

Watch Compose build and run your app with the correct parameters, including linking the relevant containers together.

$$ cat docker-compose.yml
version: "2"

services:
  www:
        build: www
        ports:
          - 8000:5000
        user: nobody
        environment:
          DEBUG: 1
        command: python counter.py
        volumes:
          - ./www:/src

  redis:
        image: redis
$ tree
.
├── docker-compose.yml
├── docker-compose.yml-ecs
├── ports.yml
└── www
        ├── assets
        │   ├── css
        │   │   ├── bootstrap.min.css
        │   │   └── bootstrap-responsive.min.css
        │   └── js
        │       └── bootstrap.min.js
        ├── counter.py
        ├── Dockerfile
        └── templates
                ├── error.html
                └── index.html

5 directories, 10 files
$ docker-compose up
Creating network "trainingwheels_default" with the default driver
Building www
Step 1/8 : FROM python
latest: Pulling from library/python
cc1a78bfd46b: Pull complete
6861473222a6: Pull complete
7e0b9c3b5ae0: Pull complete
3ec98735f56f: Pull complete
9b311b87a021: Pull complete
048165938570: Pull complete
1ca3d78efb22: Pull complete
0f6c8999c3b7: Pull complete
5a85410f5000: Pull complete
Digest: sha256:52a2bd143faf6430b182b56a5fdeb70f26b8ca8fbd40210c3ed8a8ee1eaba343
Status: Downloaded newer image for python:latest
 ---> 29d2f3226daf
Step 2/8 : RUN pip install flask
 ---> Running in 30e9159dd9dc
Collecting flask
  Downloading https://files.pythonhosted.org/packages/7f/e7/08578774ed4536d3242b14dacb4696386634607af824ea997202cd0edb4b/Flask-1.0.2-py2.py3-none-any.whl (91kB)
Collecting itsdangerous>=0.24 (from flask)
  Downloading https://files.pythonhosted.org/packages/dc/b4/a60bcdba945c00f6d608d8975131ab3f25b22f2bcfe1dab221165194b2d4/itsdangerous-0.24.tar.gz (46kB)
Collecting Jinja2>=2.10 (from flask)
  Downloading https://files.pythonhosted.org/packages/7f/ff/ae64bacdfc95f27a016a7bed8e8686763ba4d277a78ca76f32659220a731/Jinja2-2.10-py2.py3-none-any.whl (126kB)
Collecting click>=5.1 (from flask)
  Downloading https://files.pythonhosted.org/packages/34/c1/8806f99713ddb993c5366c362b2f908f18269f8d792aff1abfd700775a77/click-6.7-py2.py3-none-any.whl (71kB)
Collecting Werkzeug>=0.14 (from flask)
  Downloading https://files.pythonhosted.org/packages/20/c4/12e3e56473e52375aa29c4764e70d1b8f3efa6682bef8d0aae04fe335243/Werkzeug-0.14.1-py2.py3-none-any.whl (322kB)
Collecting MarkupSafe>=0.23 (from Jinja2>=2.10->flask)
  Downloading https://files.pythonhosted.org/packages/4d/de/32d741db316d8fdb7680822dd37001ef7a448255de9699ab4bfcbdf4172b/MarkupSafe-1.0.tar.gz
Building wheels for collected packages: itsdangerous, MarkupSafe
  Running setup.py bdist_wheel for itsdangerous: started
  Running setup.py bdist_wheel for itsdangerous: finished with status 'done'
  Stored in directory: /root/.cache/pip/wheels/2c/4a/61/5599631c1554768c6290b08c02c72d7317910374ca602ff1e5
  Running setup.py bdist_wheel for MarkupSafe: started
  Running setup.py bdist_wheel for MarkupSafe: finished with status 'done'
  Stored in directory: /root/.cache/pip/wheels/33/56/20/ebe49a5c612fffe1c5a632146b16596f9e64676768661e4e46
Successfully built itsdangerous MarkupSafe
Installing collected packages: itsdangerous, MarkupSafe, Jinja2, click, Werkzeug, flask
Successfully installed Jinja2-2.10 MarkupSafe-1.0 Werkzeug-0.14.1 click-6.7 flask-1.0.2 itsdangerous-0.24
Removing intermediate container 30e9159dd9dc
 ---> 715be459df83
Step 3/8 : RUN pip install gunicorn
 ---> Running in 27a29e572569
Collecting gunicorn
  Downloading https://files.pythonhosted.org/packages/55/cb/09fe80bddf30be86abfc06ccb1154f97d6c64bb87111de066a5fc9ccb937/gunicorn-19.8.1-py2.py3-none-any.whl (112kB)
Installing collected packages: gunicorn
Successfully installed gunicorn-19.8.1
Removing intermediate container 27a29e572569
 ---> cd78b2130321
Step 4/8 : RUN pip install redis
 ---> Running in 637a8b3cd24b
Collecting redis
  Downloading https://files.pythonhosted.org/packages/3b/f6/7a76333cf0b9251ecf49efff635015171843d9b977e4ffcf59f9c4428052/redis-2.10.6-py2.py3-none-any.whl (64kB)
Installing collected packages: redis
Successfully installed redis-2.10.6
Removing intermediate container 637a8b3cd24b
 ---> 08766036473f
Step 5/8 : COPY . /src
 ---> 4de5b2a959d5
Step 6/8 : WORKDIR /src
Removing intermediate container 6013def61017
 ---> 54eb5e672592
Step 7/8 : CMD gunicorn --bind 0.0.0.0:5000 --workers 10 counter:app
 ---> Running in bab6ea1f334c
Removing intermediate container bab6ea1f334c
 ---> 585a2f6a0163
Step 8/8 : EXPOSE 5000
 ---> Running in 228ff16daa14
Removing intermediate container 228ff16daa14
 ---> d0ad402a2cc3
Successfully built d0ad402a2cc3
Successfully tagged trainingwheels_www:latest
WARNING: Image for service www was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Pulling redis (redis:)...
latest: Pulling from library/redis
4d0d76e05f3c: Pull complete
cfbf30a55ec9: Pull complete
82648e31640d: Pull complete
fb7ace35d550: Pull complete
497bf119bebf: Pull complete
89340f6074da: Pull complete
Digest: sha256:4aed8ea5a5fc4cf05c8d5341b4ae4a4f7c0f9301082a74f6f9a5f321140e0cd3
Status: Downloaded newer image for redis:latest
Creating trainingwheels_www_1   ... done
Creating trainingwheels_redis_1 ... done
Attaching to trainingwheels_redis_1, trainingwheels_www_1
redis_1  | 1:C 01 Jun 07:45:02.780 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis_1  | 1:C 01 Jun 07:45:02.780 # Redis version=4.0.9, bits=64, commit=00000000, modified=0, pid=1, just started
redis_1  | 1:C 01 Jun 07:45:02.780 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
redis_1  | 1:M 01 Jun 07:45:02.782 * Running mode=standalone, port=6379.
redis_1  | 1:M 01 Jun 07:45:02.782 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
redis_1  | 1:M 01 Jun 07:45:02.782 # Server initialized
redis_1  | 1:M 01 Jun 07:45:02.782 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then
reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
redis_1  | 1:M 01 Jun 07:45:02.782 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the comma
nd 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
redis_1  | 1:M 01 Jun 07:45:02.782 * Ready to accept connections
www_1    |  * Serving Flask app "counter" (lazy loading)
www_1    |  * Environment: production
www_1    |    WARNING: Do not use the development server in a production environment.
www_1    |    Use a production WSGI server instead.
www_1    |  * Debug mode: on
www_1    |  * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
www_1    |  * Restarting with stat
www_1    |  * Debugger is active!
www_1    |  * Debugger PIN: 313-495-332
www_1    | X.X.X.X - - [01/Jun/2018 07:49:36] "GET / HTTP/1.1" 200 -
www_1    | X.X.X.X - - [01/Jun/2018 07:49:36] "GET /assets/css/bootstrap.min.css HTTP/1.1" 200 -
www_1    | X.X.X.X - - [01/Jun/2018 07:49:36] "GET /assets/css/bootstrap-responsive.min.css HTTP/1.1" 200 -
www_1    | X.X.X.X - - [01/Jun/2018 07:49:36] "GET /assets/js/bootstrap.min.js HTTP/1.1" 200 -
www_1    | X.X.X.X - - [01/Jun/2018 07:49:36] "GET /favicon.ico HTTP/1.1" 404 -
www_1    | X.X.X.X - - [01/Jun/2018 07:49:36] "GET /favicon.ico HTTP/1.1" 404 -
www_1    | X.X.X.X - - [01/Jun/2018 07:49:36] "GET /favicon.ico HTTP/1.1" 404 -

Stopping the app

When you hit ^C, Compose tries to gracefully terminate all of the containers.

After ten seconds (or if you press ^C again) it will forcibly kill them.

^CGracefully stopping... (press Ctrl+C again to force)
Stopping trainingwheels_www_1   ... done
Stopping trainingwheels_redis_1 ... done

The docker-compose.yml file

Here is the file used in the demo:

version: "2"
services:
  www:
        build: www
        ports:
          - 8000:5000
        user: nobody
        environment:
          DEBUG: 1
        command: python counter.py
        volumes:
          - ./www:/src
  redis:
        image: redis
$ cat www/Dockerfile
FROM python
RUN pip install flask
RUN pip install gunicorn
RUN pip install redis
COPY . /src
WORKDIR /src
CMD gunicorn --bind 0.0.0.0:5000 --workers 10 counter:app
EXPOSE 5000

Compose file versions

Version 1 directly has the various containers (www, redis…) at the top level of the file.

Version 2 has multiple sections:

  • version is mandatory and should be “2”.

  • services is mandatory and corresponds to the content of the version 1 format.

  • networks is optional and indicates to which networks containers should be connected. (By default, containers will be connected on a private, per-app network.)

  • volumes is optional and can define volumes to be used and/or shared by the containers.

Version 3 adds support for deployment options (scaling, rolling updates, etc.)

Containers in docker-compose.yml

Each service in the YAML file must contain either build, or image.

  • build indicates a path containing a Dockerfile.

  • image indicates an image name (local, or on a registry).

  • If both are specified, an image will be built from the build directory and named image.

The other parameters are optional.

They encode the parameters that you would typically add to docker run.

Sometimes they have several minor improvements.

Container parameters

  • command indicates what to run (like CMD in a Dockerfile).

  • ports translates to one (or multiple) -p options to map ports. You can specify local ports (i.e. x:y to expose public port x).

  • volumes translates to one (or multiple) -v options. You can use relative paths here.

For the full list, check: https://docs.docker.com/compose/compose-file/

Compose commands

We already saw docker-compose up, but another one is docker-compose build

It will execute docker build for all containers mentioning a build path.

It can also be invoked automatically when starting the application:

docker-compose up --build

Another common option is to start containers in the background:

docker-compose up -d

Check container status

It can be tedious to check the status of your containers with docker ps, especially when running multiple apps at the same time.

Compose makes it easier; with docker-compose ps you will see only the status of the containers of the current stack:

$ docker-compose ps
                 Name                       Command               State           Ports
----------------------------------------------------------------------------------------
trainingwheels_redis_1   docker-entrypoint.sh redis ...   Up      6379/tcp
trainingwheels_www_1     python counter.py                Up      0.0.0.0:8000->5000/tcp

Cleaning up (1)

If you have started your application in the background with Compose and want to stop it easily, you can use the kill command:

$ docker-compose kill

Likewise, docker-compose rm will let you remove containers (after confirmation):

$ docker-compose rm
Going to remove trainingwheels_redis_1, trainingwheels_www_1
Are you sure? [yN] y
Removing trainingwheels_redis_1...
Removing trainingwheels_www_1...

Cleaning up (2)

Alternatively, docker-compose down will stop and remove containers.

It will also remove other resources, like networks that were created for the application.

$ docker-compose down
Stopping trainingwheels_www_1 ... done
Stopping trainingwheels_redis_1 ... done
Removing trainingwheels_www_1 ... done
Removing trainingwheels_redis_1 ... done

Special handling of volumes

Compose is smart. If your container uses volumes, when you restart your application, Compose will create a new container, but carefully re-use the volumes it was using previously.

This makes it easy to upgrade a stateful service, by pulling its new image and just restarting your stack with Compose.

Compose project name

  • When you run a Compose command, Compose infers the “project name” of your app.

  • By default, the “project name” is the name of the current directory.

  • For instance, if you are in /home/zelda/src/ocarina, the project name is ocarina.

  • All resources created by Compose are tagged with this project name.

  • The project name also appears as a prefix of the names of the resources.

  • E.g. in the previous example, service www will create a container ocarina_www_1.

  • The project name can be overridden with docker-compose -p.

Running two copies of the same app

If you want to run two copies of the same app simultaneously, all you have to do is to make sure that each copy has a different project name.

You can:

  • copy your code in a directory with a different name

  • start each copy with docker-compose -p myprojname up

Each copy will run in a different network, totally isolated from the other.

This is ideal to debug regressions, do side-by-side comparisons, etc.