Docker, Part Eleven: The Guts Of Docker Compose

Wednesday 16 March 2016 at 08:00 GMT

Where were we? Ah yes, DNS resolution wasn't working.

org.flywaydb.core.api.FlywayException: Unable to obtain Jdbc connection from DataSource
(jdbc:postgresql://database:5432/bemorerandom) for user 'bemorerandom': The connection attempt failed.

We dug a little and found out that something's up in the Ubuntu configuration:

root@c61890596067:/# getent hosts database
172.18.0.2      database
root@c61890596067:/# getent ahosts database
root@c61890596067:/# getent ahostsv4 database
172.18.0.2      STREAM database
172.18.0.2      DGRAM
172.18.0.2      RAW

InetAddress.getByName internally invokes getaddrinfo in the operating system's core C library, as does getent ahosts when given a key to look up. I don't know how to fix it, but I know how to work around it.

Naming

Docker Compose names all the various components of your application prefixed with the name of the directory, stripped of any special characters. As I'm working in a directory named bemorerandom.com, that becomes my prefix. It's true of the images and containers too:

$ docker images
REPOSITORY                  TAG         IMAGE ID            CREATED             SIZE
bemorerandomcom_api         latest      9483181fd50d        10 minutes ago      643.2 MB

$ docker ps -a
CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS                     PORTS                    NAMES
5a6c2e69997d        bemorerandomcom_api     "/bin/sh -c 'java -cp"   6 minutes ago       Exited (1) 6 minutes ago                            bemorerandomcom_api_1
651122695d29        postgres                "/docker-entrypoint.s"   7 minutes ago       Up 6 minutes               5432/tcp                 bemorerandomcom_database_1

The image for my API service is bemorerandomcom_api. The container is the same, suffixed with _1. This is because Docker Compose has a mechanism for scaling containers up and down. If I want three of the same service running, I can instruct docker-compose to do that with the scale command.

Volumes work similarly.

$ docker volume ls
DRIVER              VOLUME NAME
local               bemorerandomcom_postgresql

And, of course, networks are named in the same way. If you don't create an explicit network, one named default is created for you.

$ docker network ls
NETWORK ID          NAME                      DRIVER
62e7b7e044d9        bridge                    bridge
d5f0b701f820        bemorerandomcom_default   bridge
8dd7c2e2a1b3        none                      null
c5a22fe8becc        host                      host

I have four networks. Three come out of the box—the default bridge network, the host network, which allows a container to share the host networking stack, and a null network named none which isolates a container from its peers. The fourth was created by Docker Compose, and is named bemorerandomcom_default. It's also a bridge network.

DNS resolution on a Docker bridge network allows you to use the container name for lookup, and Docker Compose creates a network alias that maps, for example, database to bemorerandomcom_database_1. These are simply abbreviations for a longer DNS name of the form <container>.<network>. And so we can look up our database in that form, which seems to work:

$ docker run --rm -it --net=bemorerandomcom_default ubuntu bash
root@f97a267c62ca:/# getent hosts database.bemorerandomcom_default
172.18.0.2      database.bemorerandomcom_default
root@f97a267c62ca:/# getent ahosts database.bemorerandomcom_default
172.18.0.2      STREAM database.bemorerandomcom_default
172.18.0.2      DGRAM
172.18.0.2      RAW

This time round, the ahosts lookup works. It's not ideal, as we're encoding the project name into the Docker Compose file, but we can use that:

services:
  api:
    build:
      context: .
      dockerfile: api.Dockerfile
    ports:
      - 8080:8080
    environment:
      - DB_HOST=database.bemorerandomcom_default
      - DB_NAME=bemorerandom
      - DB_USER=bemorerandom
      - DB_PASSWORD
    depends_on:
      - database

The important line is here:

      - DB_HOST=database.bemorerandomcom_default

Now let's start it up.

$ docker-compose up
...
api_1      | I 0313 19:26:06.990 THREAD1: An exception was caught and reported.
Message: org.postgresql.util.PSQLException: FATAL: role "bemorerandom" does not exist
...
api_1      | Exception thrown in main on startup
bemorerandomcom_api_1 exited with code 1

Containing Scripts

Oh, wonderful. We forgot to port the database initialisation. It turns out we can't get rid of that script completely. In order to have Docker Compose spin it up with everything else, we'll need to put it into a container, just like everything else.

Let's give it its own directory, init-db, and create a Dockerfile:

FROM postgres
WORKDIR /app
COPY * ./
CMD ./init-db.sh

Simplest Dockerfile ever, right? We're using the postgres image so we can still use psql in the script, except now we need to talk to another container. Telling psql where the database instance lives is as simple as passing it the -h and -p switches for the host and port respectively. Our commands will look the same, but our $PSQL variable now looks like this:

PSQL="psql -h $DB_HOST -p $DB_PORT -U postgres -At"

Rather than hard-coding the database information, we'll pass them in through environment variables.

Now we can add it to the Docker Compose file:

services:
  ...
  init-db:
    build: init-db
    environment:
      - DB_HOST=database
      - DB_NAME=bemorerandom
      - DB_USER=bemorerandom
      - DB_PASSWORD
    depends_on:
      - database

Right. Let's try again.

$ docker-compose up
Creating volume "bemorerandomcom_postgresql" with default driver
Creating bemorerandomcom_database_1
Creating bemorerandomcom_init-db_1
Creating bemorerandomcom_api_1
Attaching to bemorerandomcom_database_1, bemorerandomcom_init-db_1, bemorerandomcom_api_1
database_1 | LOG:  database system was shut down at 2016-03-13 19:33:30 UTC
init-db_1  | Created the PostgreSQL user bemorerandom.
database_1 | LOG:  MultiXact member wraparound protections are now enabled
init-db_1  | CREATE ROLE
database_1 | LOG:  database system is ready to accept connections
database_1 | LOG:  autovacuum launcher started
init-db_1  | CREATE DATABASE
init-db_1  | Created the PostgreSQL database bemorerandom.
bemorerandomcom_init-db_1 exited with code 0
...

It's a bit interspersed, but we can see that the role and the database were created, and then the container finished. Next time, it'll just exit immediately.

...
api_1      | INF ApiServerMain$            http server started on port: 8080
api_1      | INF ApiServerMain$            Enabling health endpoint on port 9990
api_1      | INF ApiServerMain$            App started.
api_1      | INF ApiServerMain$            Startup complete, server ready.

And we're up! Sweet. Let's test it.

$ http $(docker-machine ip):8080/xkcd
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 101
Content-Type: application/json;charset=utf-8

{
    "attribution": {
        "name": "xkcd",
        "uri": "https://xkcd.com/221/"
    },
    "random": {
        "number": 4
    }
}

Excellent. By using Docker Compose to manage our containers, we can worry less about how to start them and more about what they are. If we start it in daemon mode with docker-compose up -d, they'll run in the background, and we can simply rebuild and recreate them as necessary:

$ docker-compose build && docker-compose up -d

If we add new containers or change the way things need to be built, that'll still work. It just goes into the docker-compose.yml file, versioned with everything else. I love my shell scripts, but I'd still much rather that things worked well without them.


If you enjoyed this post, you can subscribe to this blog using Atom.

Maybe you have something to say. You can email me or toot at me. I love feedback. I also love gigantic compliments, so please send those too.

Please feel free to share this on any and all good social networks.

This article is licensed under the Creative Commons Attribution 4.0 International Public License (CC-BY-4.0).