Docker Compose Tutorial - A Quick Primer
A quick primer on Docker Compose
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
(theDockerfile
) you’re using is located. This single dot (.
) means the current directory (just like you’d type in yourshell
).
- This is where (the file path of)
dockerfile
:- This is the filename of your Dockerfile (if it’s not
Dockerfile
). In our example, we’re using a Dockerfile namedDockerfile.dev
.
- This is the filename of your Dockerfile (if it’s not
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!