Published: 13.06.2020 | Edited: 22.12.2020 | Tags: docker

Why to use labels in docker-compose

Recently I had faced an apparently easy to solve problem that however became little bit trickier in the end. Imagine an application that connects to the database. Nothing super special fancy here. The docker-compose.yml file could for instance look something like this:

#
# The variables come from the .env file
#
version: '3.2'
services:
  db:
  image: postgresql:12
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_NON_ROOT_USER: ${POSTGRES_USER}
      POSTGRES_NON_ROOT_USER_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ${STORAGE_PATH}/${INSTANCE_NAME}/db:/var/lib/postgresql/data
  app:
  image: myapp:1
    depends_on:
        - db
    environment:
      INSTANCE_NAME: ${INSTANCE_NAME}
      DB_HOST: db
      DB_NAME: ${POSTGRES_DB}
      DB_USER: ${POSTGRES_USER}
      DB_PASSWORD: ${POSTGRES_PASSWORD}
    links:
      - db:db
    volumes:
      - ${STORAGE_PATH}/${INSTANCE_NAME}/data:/srv/web/data
    ports:
      - "${INSTANCE_PORT}:8080"

We see that the app container is being run alongside the db one. After starting the service with docker-compose up -d we would like to verify that both containers are running and the ports they expose (or in case of database hide). One of the way this could be achieved is to utilize the --format argument of the docker ps command as follows:

docker ps --format "table {{.Names}}\t{{.Ports}}"

The result of this command could look like this:

NAMES      PORTS
app_1      0.0.0.0:8080->8080/tcp
db_1       5432/tcp

Adding more daemons

One of the advantages of the Docker ecosystem is that you can scale the services horizontally. So if we wanted to run another instance of the service on the same host, we could just copy the service directory and tweak the variables in the .env file. Two variables that must be changed are INSTANCE_NAME and specially INSTANCE_PORT. Changing port is needed because otherwise the containers would like to bind to the same port, which is obviously not what we want.

This daemon could be started the same way as the previous one. To observe the running containers we also tweak our docker ps command a little bit to differentiate between the containers belonging to the different services:

docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Ports}}"

Output could be similar to this:

CONTAINER ID        NAMES      PORTS
0cd27fa1c363        app_1      0.0.0.0:8080->8080/tcp
bd2832c8f6fc        app_1      0.0.0.0:8081->8080/tcp
057a4002ce66        db_1       5432/tcp
347a51256c16        db_1       5432/tcp

Note that the apps are bound to the ports 8080 an 8081 on the host system.afsdsfdThere is one big problem in this approach. Apart from the container ID however, depending on other aspects of the container configuration, there could be nothing easily accessible in the whole docker ps output that would help us really differentiate in a human readable form what are those containers all about.

Labels to the rescue!

One way around this problem is to use so called labels. Labels allow to set metadata to most Docker objects, including:

  • Images
  • Containers
  • Local daemons
  • Volumes
  • Networks
  • Swarm nodes
  • Swarm services

Citing the documentation, labels can be used to to organize your images, record licensing information, annotate relationships between containers, volumes, and networks, or in any way that makes sense for your business or application.

You can easily specify labelafsdsfdduring runtime, but to make it more persistent inside Dockerfile via LABEL instruction:

LABEL instance=red # or instance=blue

After assigning the labels and altering our format parameter to include the labels (which are hidden by default):

docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Ports}}\t{{.Labels}}"

The output can look like this:

CONTAINER ID        NAMES      PORTS                      LABELS
0cd27fa1c363        app_1      0.0.0.0:8080->8080/tcp     instance=red
bd2832c8f6fc        app_1      0.0.0.0:8081->8080/tcp     instance=blue
057a4002ce66        db_1       5432/tcp
347a51256c16        db_1       5432/tcp

Now this is a nice approach when the Dockerfile we use is under our control, but this is not always the case. I would argue that this is more of an exception than a norm.

Dockerfile is not my own

This issue was solved in Compose file version 3.3. ou can look up the details in its documentation. Adding labels into Compose file is more convenient when the service is distributed to you through it.

For the completenes, the edited Compose file could look like this:

#
# The variables come from the .env file
#
version: '3.3' # version bumped to 3.3 or higher
services:
  db:
  image: postgresql:12
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_NON_ROOT_USER: ${POSTGRES_USER}
      POSTGRES_NON_ROOT_USER_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ${STORAGE_PATH}/${INSTANCE_NAME}/db:/var/lib/postgresql/data
  app:
  image: myapp:1
    depends_on:
        - db
    environment:
      INSTANCE_NAME: ${INSTANCE_NAME}
      DB_HOST: db
      DB_NAME: ${POSTGRES_DB}
      DB_USER: ${POSTGRES_USER}
      DB_PASSWORD: ${POSTGRES_PASSWORD}
    links:
      - db:db
    volumes:
      - ${STORAGE_PATH}/${INSTANCE_NAME}/data:/srv/web/data
    ports:
      - "${INSTANCE_PORT}:8080"
    labels: # labels in Compose file instead of Dockerfile
      - "instance-name": ${INSTANCE_NAME}

This way, when displaying labels via the docker ps command used last time, the instance name is also visible.