Featured image of post Docker compose tips & tricks

Docker compose tips & tricks

Gather useful how-to to operate docker ecosystem as its best.

Docker ecosystem is truly amazing. This said, very few users take advantages of all its built-in and often hidden facilities. Idea there is to both increase awareness of such features while providing real world usage.

Dry config using extension

Configuration materials such as compose files or Kubernetes manifests are inherently verbose. Networking setup is something that we encounter in every compose file. Ideally, we want to allocate a dedicated network four our services to operate on, as well as providing bridge for the OS-based executables to interact with. It translates into following bits, that have to be cloned for every single service part of the deployment. It is painful to write, painful to read and painful to maintain.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
services:

  frontend:
    image: frontend:latest
    ports:
      - 9611:80
    networks:
        - custom_nw
    extra_hosts:
        - "host.docker.internal:host-gateway"

  backend:
    image: backend:latest
    ports:
      - 9610:9610
    networks:
        - custom_nw
    extra_hosts:
        - "host.docker.internal:host-gateway"

networks:
  custom_nw:

One way of DRYing this part is to leverage extension mechanism:

  • Declare common settings through an extension, here x-app
  • Mark it with an anchor, here default-glue
  • Inject settings into through alias, here <<: *default-glue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
x-app:
  &default-glue
  networks:
    - custom_nw
  extra_hosts:
    - "host.docker.internal:host-gateway"

services:

  frontend:
    <<: *default-glue
    image: frontend:latest
    ports:
      - 9611:80

  backend:
    <<: *default-glue
    image: backend:latest
    ports:
      - 9610:9610

networks:
  custom_nw:

Below the diff view to better assess changes. Of course, the more you have services the better shines the pattern.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
+ x-app:
+   &default-glue
+   networks:
+     - custom_nw
+   extra_hosts:
+     - "host.docker.internal:host-gateway"

services:

  frontend:
+    <<: *default-glue
    image: frontend:latest
    ports:
      - 9611:80
-    networks:
-        - custom_nw
-    extra_hosts:
-        - "host.docker.internal:host-gateway"

  backend:
+    <<: *default-glue
    image: backend:latest
    ports:
      - 9610:9610
-    networks:
-        - custom_nw
-    extra_hosts:
-        - "host.docker.internal:host-gateway"

networks:
  custom_nw:

Compose multiple docker compose files

Assume you have an instrumented stack you would like to be able to deploy in 2 flavors:

  1. Deploy the stack only, with OTEL flag switched off (default value)
  2. Deploy the stack, with OTEL flag switched on, along with the OTEL ecosystem

There are multiple ways of doing that, such as profiles or compose merge.
Profile-based solution works great but tends to noise the original compose file, making difficult to easily assess what is deployed and what isn’t.
On the other hand, merge-based solution allows for better separation of concern, in a way Open/Closed Principle is applied to code.

Default compose.yaml focuses on deploying the bare-metal stack the way we are used to

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# compose.yaml
services:

  frontend:
    image: frontend:latest
    ports:
      - 9611:80

  backend:
    image: backend:latest
    ports:
      - 9610:9610

Deploying stack through docker CLI

1
docker compose up

As we have previously instrumented our code base, enabling user to switch on or off this facility through command line flag, --otel, we can draft another compose file to gather this configuration. We start by complementing our stack declared in the core compose file using matching service alias. Here, we activate the flag and provide expected OTEL environment variables. Then, we allocate new OTEL services.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# compose.otel.yaml
services:

  frontend:
    command:
      - --otel
    environment:
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
      - OTEL_EXPORTER_OTLP_METRICS_INSECURE=true
      - OTEL_EXPORTER_OTLP_TRACES_INSECURE=true

  backend:
    command:
      - --otel
    environment:
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://tracelens:4317
      - OTEL_EXPORTER_OTLP_METRICS_INSECURE=true
      - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://tracelens:4317
      - OTEL_EXPORTER_OTLP_TRACES_INSECURE=true

  otel-collector:
    <<: *default-glue
    image: otel/opentelemetry-collector-contrib:0.80.0
    command: [ "--config=/etc/otel-collector-config.yaml" ]
    volumes:
      - ./deployment/compose/otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "8888:8888"   # Prometheus metrics exposed by the collector
      - "8889:8889"   # Prometheus exporter metrics
      - "4318:4318"   # OTLP http receiver
      - "13133:13133" # Health check
    depends_on:
      - tracelens

Deploying stack through docker CLI by chaining compose files.

1
2
3
4
docker compose `
    -f compose.yaml `
    -f compose.otel.yaml `
    up

Of course, we can use this pattern for other uses cases such as:

  • Provide compose overrides for dev, staging and production environment
  • Provide compose overrides per customer

Remember the smarter your original stack is articulated and instrumented upstream, the easier it will be to compose and accommodate new use cases downstream without having to modify the base materials.

Licensed under CC BY-NC-SA 4.0
Last updated on Dec 08, 2023 00:00 UTC
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy