Stop Writing Mediocre Docker-Compose Files
By: Meysam Azad
Tags: Programming, Coding, Docker, Learning, Data Science

Boy oh boy: the service

The most important part of any docker-compose file is the service section, which describes the manner in which services should boot up. So without further ado, let’s get to it.

Every service starts with a name, and it’s very likely that you want your services to have a related name to what they are doing. They should also be unique in the scope of all the services inside a docker-compose.yml .

You can also run multiple compose files at once using the -f flag when running the command docker-compose up . but for simplicity’s sake, we assume all your services are inside a single docker-compose.yml .

So:

services:

postgres:

...

...

cool_app:

...

...

image

This attribute is self-explanatory, and unless you’re using the build attribute, it’s a must-have. I don’t need to tell you to ALWAYS tag your images, do I? You never want to run a service using the latest tag (which is the default, if you don’t mind me saying).

This attribute can also be used to name an image after building it (we talk about this later in this article).

So:

services:

postgres:

image: postgres:12.1

...

...

container_name

This is very useful for your own sake. Has it ever happened to you that you’ve queried the containers of a system using docker ps ? Then I guess you’re familiar with the name great_yonath , or perhaps youthful_black etc. Now let me tell you that I’m not racist, and these are just the names that docker engine assigned to my containers when I didn’t explicitly specify the name using the container_name attribute. Therefore, Always specify.

So:

services:

postgres:

...

container_name: cool_app-postgres

...

Do I need to tell you that I chose an arbitrary name and you can choose your own too? I don’t think so.

hostname

When trying to craft multiple services into one, your main reason for doing that is not because you’re lazy to write a new docker-compose.yml file (I hope so 😉), but rather because the services are going to have communications to accomplish a certain task.

If that is the case, then you surely need this attribute, because when a web app running in a container trying to connect to a database asks for the name cool_app-database , docker engine would resolve that name to the IP address assigned to the service above. So always specify this if you want your database connection to work.

So:

services:

postgres:

...

hostname: cool_app-database

...

build

If you want to specify the location of your Dockerfile , you surely can, and the docker engine would build your image using the information in build attribute, if no such image is found on the system. And the name of the image would come from image attribute mentioned above.

This attribute has some important children that should be mentioned here:

context : This specifies the location in which you would otherwise cd into before using docker build . target : This is most useful for people who use multi-stage builds. You can specify which stage you’re interested in building with this. dockerfile : Self-explanatory I hope, this attribute is the path (absolute, or relative to context ) of the Dockerfile if it’s name is not the typical Dockerfile e.g. Dockerfile.dev . It’s optional and uses Dockerfile by default.

So:

services:

cool_app:

...

build:

context: .

target: prod

dockerfile: Dockerfile.prod

...

You can figure out that . is relative to the location of the docker-compose.yml .

If you want to find out the best practices of writing a Dockerfile , take a look at my other article about the topic:

volumes

This is an array, as I suppose you would know, and I recommend that you refrain from specifying anything other than the name of the docker volume. Because if you specify a path in here, every file in that directory would change permission and your host’s user would lose access to them. And on critical occasions you would need to ask for the help of Superuser to get your hands on the files.

So:

services:

postgres:

...

volumes:

- "./db_data:/var/lib/postgresql/data" # NOT RECOMMENDED

- "cool_app-database:/var/lib/postgresql/data" # BETTER

...

...

volumes:

cool_app-database: # just a declaration, nothing more needed

When specifying a name for your volume, you should also define your volumes somewhere outside services , which will be discussed further below.

You can also mount a path on the container with read-only permission, to keep the container from changing the owner of the files and therefore losing access to them, leading to asking for the help of Superuser again.

So:

services:

cool_app:

...

volumes:

- "./models:/service/models:ro" # example AI service

...

Image by nickjanetakis.com

restart

This is useful to dictate how you want your containers to restart. Available options include “no”, “always”, “on-failure”, “unless-stopped”. The last one is my favorite, which is a super set of “on-failure”.

So:

services:

postgres:

...

restart: unless-stopped

...

There is also a restart_policy under the deploy attribute, but that is not useful unless you’re running your services inside a swarm (I might write about it later).

depends_on

I use this attribute a lot, when I want one of my containers to be tied to another service, operationally & developmentally speaking. This attribute helps docker engine distinguish which container to start first. You can use it to start your database before your application.

So:

services:

postgres:

... cool_app:

...

depends_on:

- postgres

...

environment

This helps you pass environmental variables to the docker container. For example when you want to specify the connection string of a database for your application, or when you want to change the default port to be published inside the container etc. This can be achieved in one of the following two ways.

Using pair-values explicitly:

services:

postgres:

...

environment:

PORT: "8000"

...

Also possible using this notation:

environment:

- PORT="8000"

2. Passing the environmental variables from the host’s context — which is possible through one of the following two ways:

If an environmental variable is set in the current shell e.g. using the command export PORT=8000 .

. Having a .env file in the same location as the docker-compose.yml and specifying the key-value pairs inside that:

# .env

PORT=8000

And then you can simply pass that using the following pattern:

services:

cool_app:

...

environment:

- PORT

...

This would fetch the value of a variable named PORT either in .env , or in the variables from the context of the current shell.

environment is a very cool feature of docker, make sure to use it a lot.

ports

No need further explanation, but there are 2 ways you can use this. Either short syntax, or long syntax. Most of the times you don’t need anything more than short syntax — it’s concise, elegant and right to the target.

So:

services:

cool_app:

...

ports:

# either publish on all interfaces:

- "8000:8000" # or only publish on localhost:

- "127.0.0.1:8000:8000"

According to the documentation, try to ALWAYS specify ports inside double quotations.

When mapping ports in the HOST:CONTAINER format, you may experience erroneous results when using a container port lower than 60, because YAML parses numbers in the format xx:yy as a base-60 value. For this reason, we recommend always explicitly specifying your port mappings as strings.

networks

Some people ignore using this attribute, as docker assigns all the services inside a docker-compose.yml into the same network, and they don’t feel the need to explicitly specify it.

But doing so has it’s own advantages:

No ugly default network name which happens by default when you don’t specify one. You don’t take any chance and you don’t let anything surprise you. This is most desirable in production environments, when you’re narrowing down your uncontrolled parameters to the bare minimum.

So:

services:

cool_app:

...

networks:

- cool_app

...

And there you have it, all the necessary stuff you need to take your services inside docker-compose to the next level presented to you.

After the services, there are volumes and networks.