Extending Docker Compose


We use the `docker-compose` ability to `extend` other files to create a different environment for development and continuous integration.

We'll cover a few things:

  • Expanding on our use of default environment variables
  • Separating our compose configurations for multiple environments

Environment Variables

The first thing we'll do is expand on our default environment variables to take the MySQL database setup into account.

The first time the mysql container is spun up on a developer's machine, these defaults will be used to set the passwords and a database to use:

#!/usr/bin/env bash

# Set environment variables for dev
export APP_ENV=${APP_ENV:-local}
export APP_PORT=${APP_PORT:-80}
export DB_ROOT_PASS=${DB_ROOT_PASS:-secret}
export DB_NAME=${DB_NAME:-helpspot}
export DB_USER=${DB_USER:-helpspot}
export DB_PASS=${DB_PASS:-secret}

COMPOSE="docker-compose"

# Rest of bash script truncated for brevity...

And then we can update our docker-compose.yml file, in the mysql service (file truncated for brevity):

version: '2'
services:
  mysql:
    extends:
        file: docker-compose.base.yml
        service: mysql
    ports:
     - "${DB_PORT}:3306"
    environment:
      MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASS}"
      MYSQL_DATABASE: "${DB_NAME}"
      MYSQL_USER: "${DB_USER}"
      MYSQL_PASSWORD: "${DB_PASS}"

Docker-Compose for Dev and CI

For CI, we don't want any port binding. In fact, we may not want to even spin up a MySQL container. This, of course, is the opposite of what we want for local development. So, let's accomplish changing how we run Docker in devopment vs continuous integration.

File docker-compose.base.yml

We first create a base compose file that other files will extend:

version: '2'
services:
  app:
    build:
      context: ./docker/app
      dockerfile: Dockerfile
    image: shippingdocker.com/app
    volumes:
     - .:/var/www/html
    networks:
     - sdnet
  node:
    build:
      context: ./docker/node
      dockerfile: Dockerfile
    image: shippingdocker.com/node
    volumes:
     - .:/var/www/html
    networks:
     - sdnet
  mysql:
    image: mysql:5.7
    volumes:
     - mysqldata:/var/lib/mysql
    networks:
     - sdnet
  redis:
    image: redis:alpine
    volumes:
     - redisdata:/data
    networks:
     - sdnet

File docker-compose.ci.yml

Then we can make a docker-compose file to extend this base file. Note we remove

version: '2'
services:
  app:
    extends:
      file: docker-compose.base.yml
      service: app
  node:
    extends:
      file: docker-compose.base.yml
      service: node
  redis:
    extends:
      file: docker-compose.base.yml
      service: redis
networks:
  sdnet:
    driver: "bridge"
volumes:
  mysqldata:
    driver: "local"
  redisdata:
    driver: "local"

File docker-compose.dev.yml

Finally we make our docker-compose file for development, which includes MySQL and our port bindings.

version: '2'
services:
  app:
    extends:
      file: docker-compose.base.yml
      service: app
    ports:
     - "${APP_PORT}:80"
  node:
    extends:
      file: docker-compose.base.yml
      service: node
  mysql:
    extends:
        file: docker-compose.base.yml
        service: mysql
    ports:
     - "${DB_PORT}:3306"
    environment:
      MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASS}"
      MYSQL_DATABASE: "${DB_NAME}"
      MYSQL_USER: "${DB_USER}"
      MYSQL_PASSWORD: "${DB_PASS}"
  redis:
    extends:
      file: docker-compose.base.yml
      service: redis
networks:
  sdnet:
    driver: "bridge"
volumes:
  mysqldata:
    driver: "local"
  redisdata:
    driver: "local"

Running Commands

We can see that running commands with these adds yet more boiler plate:

APP_PORT=80 docker-compose -f docker-compose.dev.yml ps

So let's update our develop script to make use of these options:

Updating the develop Script

The updated develop script, which knows if we're in a Jenkins build (CI) and decides which compose file to run based on that:

#!/usr/bin/env bash

# Set environment variables for dev
export APP_ENV=${APP_ENV:-local}
export APP_PORT=${APP_PORT:-80}
export DB_PORT=${DB_PORT:-3306}
export DB_ROOT_PASS=${DB_ROOT_PASS:-secret}
export DB_NAME=${DB_NAME:-helpspot}
export DB_USER=${DB_USER:-helpspot}
export DB_PASS=${DB_PASS:-secret}

# Decide which docker-compose file to use
COMPOSE_FILE="dev"

# Change settings for CI
if [ ! -z "$BUILD_NUMBER" ]; then
    COMPOSE_FILE="ci"
fi

# Create docker-compose command to run
COMPOSE="docker-compose -f docker-compose.${COMPOSE_FILE}.yml"

# If we pass any arguments...
if [ $# -gt 0 ];then
    # If "art" is used, pass-thru to "artisan"
    # inside a new container
    if [ "$1" == "art" ]; then
        shift 1
        $COMPOSE run --rm \
            -w /var/www/html \
            app \
            php artisan "$@"

    # If "composer" is used, pass-thru to "composer"
    # inside a new container
    elif [ "$1" == "composer" ]; then
        shift 1
        $COMPOSE run --rm \
            -w /var/www/html \
            app \
            composer "$@"

    # If "test" is used, run unit tests,
    # pass-thru any extra arguments to php-unit
    elif [ "$1" == "test" ]; then
        shift 1
        $COMPOSE run --rm \
            -w /var/www/html \
            app \
            ./vendor/bin/phpunit "$@"

    # If "npm" is used, run npm
    # from our node container
    elif [ "$1" == "npm" ]; then
        shift 1
        $COMPOSE run --rm \
            -w /var/www/html \
            node \
            npm "$@"

    # If "gulp" is used, run gulp
    # from our node container
    elif [ "$1" == "gulp" ]; then
        shift 1
        $COMPOSE run --rm \
            -w /var/www/html \
            node \
            ./node_modules/.bin/gulp "$@"
    # Else, pass-thru args to docker-compose
    else
        $COMPOSE "$@"
    fi

else
    $COMPOSE ps
fi

Testing It Out

We can see which is being selected using the config command with docker-compose:

# Make sure it's basically working
./develop ps

# See that it defaults to "dev" - we'll see
# the mysql service and our port bindings
./develop config

# Make sure we can force it to use the CI config
# We'll see no port bindings and no mysql service
BUILD_NUMBER=1234 ./develop config