Bite-Sized Dev Tips

Bite-Sized Dev Tips

The goal is to help you, the developer, by turning complex things into understandable, bite-sized pieces.

Docker Compose Tutorial - A Quick Primer

A quick primer on Docker Compose

Geoff Safcik

9-Minute Read

Docker Compose Tutorial - A Quick Primer

NOTE: this post will be more for Docker Desktop for Mac but this is applicable across devices.

Today, I wanted to give a quick primer on Docker Compose.

What is Docker Compose?

I’ve heard of Docker but what is Docker Compose?

Well, that’s a good question if you’re new to this. Straight from the docs:

Compose is a tool for defining and running multi-container Docker applications.

I couldn’t have said it better myself.

This is a very useful tool to help you build out your Dockerized applications. Even if you’re building out microservices, you can get up and running quickly with docker-compose.

We’ll be diving into Docker Compose from the context of development, remembering that this blog tries to keep things in “bite-sized pieces”. I’ll try to make things as succinct as possible but this is a big topic so, by necessity, this article will be a longer one.

How do we get started?

First, you need to know if it’s already included with your Docker software. If you’re running Docker Desktop for Mac (or Docker Desktop for Windows), it’s already included!

If not, you can run docker-compose --version on your CLI to see if you have it. If not, see the installation instuctions to get it (click on your OS’ tab)!

Once you have it, you’ll need to define a set of instructions for docker-compose to read and run. This set of instructions is found in this file: docker-compose.yml. You’ll need to create this file and put it in the root of your project/application.

Once you have this file in the root directory of your project application, you’ll need to fill it out (yay!).

Anatomy / Commands

It starts with a version number for the Docker Compose tool (see versions here). In other words, the first thing you’ll write in the file is something like this:

version: '3'

Next, you’ll want to define your services, volumes, and networks. Let’s start with services. On the next level (1 indent), you’ll need to define the name of your service(s). For example, adding to our file:

version: '3'
# added everything below this comment
services:
  my_service_name:

NOTE: you can create more than one service.

We’ll explore the services options in more detail now before we go onto the other options.

The services

There are a lot of services options. You can view them all in the docs. For the purposes of this post, I want to concentrate on a few (just to get you going). But please, check out all the options as understanding them will help you a lot!

After you have the name of your service, you’ll define its options. A few I want to mention are:

  • build
  • image
  • ports
  • volumes
  • environment
  • networks
The build option

build tells docker-compose how you want to build your container (e.g. what Dockerfile to use and where is it located). You really only need to use this option if you use more than one Dockerfile. You can omit this option altogether and it will use the Dockerfile in the root directory of you app (same location as your docker-compose.yml file). For demonstration, say you have a Dockerfile.dev file you use for development (you will see this out in the real world). Continuing our file, here’s how to handle that:

version: '3'
services:
  my_service_name:
    # added everything below this comment
    build:
      context: .
      dockerfile: Dockerfile.dev

From the Docker build docs:

build can be specified either as a string containing a path to the build context…

Or, as an object with the path specified under context and optionally Dockerfile and args

Here’s a quick overview of what we’ve defined in the build argument (which we’ve defined as an object):

  • context:
    • This is where (the file path of) Dockerfile.dev (the Dockerfile) you’re using is located. This single dot (.) means the current directory (just like you’d type in your shell).
  • dockerfile:
    • This is the filename of your Dockerfile (if it’s not Dockerfile). In our example, we’re using a Dockerfile named Dockerfile.dev.

A docker command equivalent:

docker build -f Dockerfile.dev

Please reference the build docs for further formatting and options. Moving on…

The image option

This option is used in conjunction with the build option. The image option is the tag (or name) that Docker Compose will give the image built from the process. This way, you have full control over the tag/name of the image. Building onto what we already have:

version: '3'
services:
  my_service_name:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: repo/my-service-image:0.1.0  # added

A docker command equivalent:

docker build -t <tag>

That’s it. Moving on…

The ports option

You can define ports to map and open up that your service will use. This is a yaml array or strings so you’ll need to set it up like the following (adding onto what we already have):

version: '3'
services:
  my_service_name:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: repo/my-service-image:0.1.0
    # added everything below this comment
    ports:
      - "8080:8080"

You can add more to the array if you’d like.

A docker command equivalent:

docker run -p 8080:8080 <image>

Pretty straight forward, moving on…

The volumes option

Mount host paths or named volumes, specified as sub-options to a service.

What are you going to mount (and share) with your docker container? Well, define it here. For instance, building onto the file we have already:

version: '3'
services:
  my_service_name:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: repo/my-service-image:0.1.0
    ports:
      - "8080:8080"
    # added everything below this comment
    volumes:
      - "~/.aws/:/root/.aws:ro"

I hear what you’re saying.

What did he add there?

If you’ve never worked with AWS services before, I added the .aws/ directory to the contianer so that it can connect to AWS services automagically (don’t worry about the exact details of that). It’s pretty common for your dev environment to do this during development if you work with AWS services (and remember, development is our focus here). The important things to note are:

  • this option is an array of strings (like ports)
  • The colon (:) separates the paths.
    • left-side of the colon is your local computer’s path to the directory
    • right-side of the colon is your container’s path to the directory

A docker command equivalent:

docker run -v <local_path>:<container_path> <image>
The environment option

This is the option for environment variables! That’s it.

This option is also an array of strings. Adding onto our file…

version: '3'
services:
  my_service_name:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: repo/my-service-image:0.1.0
    ports:
      - "8080:8080"
    volumes:
      - "~/.aws/:/root/.aws:ro"
    # added everything below this comment
    environment:
      - LOCAL_ENV=true
      - USE_TLS=false

Ok, I made up some crummy environment vars…😒…but the concept is straight forward. Add your environment vars here 😄.

A docker command equivalent:

docker run -e LOCAL_ENV=true -e USE_TLS=false <image>
The networks option

This option defines the networks you want Docker Compose to create for the service on your behalf. There’s a reason this is last of the options I want to cover. This is because it ties into the top-level networks option.

This option is, again, an array of strings. Let’s see how it’s defined (of course, building onto our file):

version: '3'
services:
  my_service_name:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: repo/my-service-image:0.1.0
    ports:
      - "8080:8080"
    volumes:
      - "~/.aws/:/root/.aws:ro"
    environment:
      - LOCAL_ENV=true
      - USE_TLS=false
    # added everything below this comment
    networks:
      - main_network
      - peripheral_network

We’re telling Docker Compose to create two networks for our my_service_name service (named main_network and peripheral_network).

A docker command equivalent:

$ docker network create <folder>_main_network
$ docker network create <folder>_peripheral_network

WAIT! Why is the equivalent name different?

<folder> here refers to the main directory name that the docker-compose.yml file is in. It gets prepended, with an underscore (_), to the network name(s) you define.

For example, if your project is in a folder called my-service, then that would equate to the network names of:

my-service_main_network
my-service_peripheral_network

That’s it. We’ll dig more into the aspects of the network since it’s a really import aspect of Docker/Docker Compose.

The networks

There aren’t nearly as many options for networks as services but you can view them all in the docs. For the purposes of this post, I won’t actually go into any. But, again, please check out all the options as understanding them will help you a lot!

You’ll want to define the same names you created in your services networks option in the next indentation level (just like the services option). For instance, building on to our existing file:

version: '3'
services:
  my_service_name:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: repo/my-service-image:0.1.0
    ports:
      - "8080:8080"
    volumes:
      - "~/.aws/:/root/.aws:ro"
    environment:
      - LOCAL_ENV=true
      - USE_TLS=false
    networks:
      - main_network
      - peripheral_network
# added everything below this comment
networks:
  main_network:
  peripheral_network:

At the very least, if you define any networks in the under a service name within services, you’ll need to define it here.

After you have the name(s) of your network(s), you can define their options but for basic use you won’t need any options.

I will cover options like connecting up a database, connecting up different services in different repositories together (using Docker Compose), and even the Dockerfile itself…

…but that’s not in the scope of this article.

The volumes

From the docs:

While it is possible to declare volumes on the fly as part of the service declaration, this section allows you to create named volumes that can be reused across multiple services (without relying on volumes_from), and are easily retrieved and inspected using the docker command line or API.

I highly recommend checking out this section of the docs as you’ll need to understand this more for some very useful functionality (including sharing a database’s data across two services)!

You can define them as follows (continuing our file…):

version: '3'
services:
  my_service_name:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: repo/my-service-image:0.1.0
    ports:
      - "8080:8080"
    volumes:
      - "~/.aws/:/root/.aws:ro"
    environment:
      - LOCAL_ENV=true
      - USE_TLS=false
    networks:
      - main_network
      - peripheral_network

networks:
  main_network:
  peripheral_network:

# added everything below this comment
volumes:
  main_network:
  peripheral_network:

We’ve just defined the names here (no options).

NOTE: above is our full file (up to this point) and it should be in a directory tree like this:

./my-service
+-- application_files
|   +-- main.py
|   +-- stuff.py
|   +-- i-dont-know.py
+-- docker-compose.yml
+-- Dockerfile
+-- Dockerfile.dev

Well, we made it to the end of the article 😌! I hope this primer is helpful to at least one person out there. Remember to visit the Docker Compose documentation links in this article to go through all the options there are and, hopefully, get a better understanding of them!

comments powered by Disqus

Recent Posts

categories