Introduction

docker-compose is a tool for defining and running multi-container Docker applications on a single node/host. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration (REF).

For installation, some linux distributions such as Debian provide a proper package with bash completion. Another solution is to download the single command-file from its GitHub using the following command (REF):

1
2
sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

Bash Completion

Make sure bash completion is installed.

1
sudo curl -L https://raw.githubusercontent.com/docker/compose/1.27.4/contrib/completion/bash/docker-compose -o /etc/bash_completion.d/docker-compose

Compose Features

  • Multiple isolated environments on a single host
    • Uses a project name to isolate environments from each other
    • The default project name is the basename of the project directory
  • Preserve volume data when containers are created
    • When docker-compose up runs, if it finds any containers from previous runs, it copies the volumes from the old container to the new container
  • Only recreate containers that have changed
    • Compose caches the configuration used to create a container. When you restart a service that has not changed, Compose re-uses the existing containers. Re-using containers means that you can make changes to your environment very quickly.
  • Variables and moving a composition between environments
    • Variables can be used in the Compose file
    • Variables from the shell environment such as ${USER} can be addressed
    • Variables also can be defined in a .env file beside the Compose file in key=value format
    • Variables can be used in the following formats
      • ${VARIABLE} or $VARIABLE - simple
      • ${VARIABLE:-default} evaluates to default if VARIABLE is unset or empty in the environment.
      • ${VARIABLE-default} evaluates to default only if VARIABLE is unset in the environment.
      • ${VARIABLE:?err} exits with an error message showing err if VARIABLE is unset or empty in the environment.
      • ${VARIABLE?err} exits with an error message showing err if VARIABLE is unset in the environment.

The .env file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
VOL_BASE_DIR=/opt/docker_vols

TRAEFIK_VER=2.1.3
PORTAINER_VER=1.23.0

BUSYBOX_VER=1.31.1-glibc
NGINX_VER=1.17.2

CONFLUENT_VER=5.3.1-1
KAFDROP_VER=3.23.0

REDIS_VER=5.0.7
REDIS_PASSWD=ReDis

MYSQL_VER=8.0.19
MYSQL_PASSWS=rOOt
ADMINER_VER=4.7.6-standalone

First Sample

In the following yml, some images are used to illustrate various docker-compose features alongside the above .env file. Before execution, run docker network create main_net.

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
version: '3.6'

services:

  traefik:
    image: traefik:${TRAEFIK_VER:?ver}
    command:
      - "--api.insecure"
      - "--providers.docker"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.http.address=:80"
    restart: always
    ports:
      - 80:80
      - 8080:8080
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - web

  portainer:
    image: portainer/portainer:${PORTAINER_VER:-latest}
    command: -H unix:///var/run/docker.sock
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.portainer.rule=PathPrefix(`/portainer`)"
      - "traefik.http.routers.portainer.middlewares=portainerRedir,portainerPStrip"
      - "traefik.http.middlewares.portainerPStrip.stripprefix.prefixes=/portainer"
      - "traefik.http.middlewares.portainerRedir.redirectregex.regex=^(.*)/portainer$$"
      - "traefik.http.middlewares.portainerRedir.redirectregex.replacement=$${1}/portainer/"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ${VOL_BASE_DIR:-.}/Portainer:/data
    networks:
      - web

  nginx:
    image: nginx:${NGINX_VER:-latest}
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nginx.rule=PathPrefix(`/web`)"
      - "traefik.http.routers.nginx.middlewares=nginxPStrip"
      - "traefik.http.middlewares.nginxPStrip.stripprefix.prefixes=/web"
    networks:
      - web

  busybox:
    image: busybox:${BUSYBOX_VER:-latest}
    hostname: busybox
    restart: always
    tty: true
    networks:
      - ext
      - web

networks:
  web:
  ext:
    external: true
    name: main_net

Copy sample in docker-compose.yml and save it in a directory called Example (just to simplify the sample), so the example is the project name. Also create .env in this directory. Now run docker-compose up -d:

  • example_web docker network is created
  • docker-compose ps - four containers are created
    • example_busybox_1
    • example_nginx_1
    • example_portainer_1
    • example_traefik_1
  • docker-compose logs -f traefik - watch the traefik service log
  • docker-compose exec busybox sh - the sh program is executed in the busybox service. Now execute following commands:
    • hostname - result: busybox (line 51)
    • ip a - result: 3 interfaces - lo, one for web and the other for ext
    • ping traefik and ping example_traefik_1 show the same result. The traefik is set as alias for container example_traefik_1 (docker container inspect -f '{{ .NetworkSettings.Networks.example_web.Aliases }}' example_traefik_1)
    • telnet traefik 80, telnet nginx 80, and telnet portainer 9000 - you can telnet the services
  • Traefik is the reverse proxy. Access the following URLs:
  • For volumes, ./DIR can be used to create a DIR in current directory on host (line 34 if VOL_BASE_DIR is undefined).
    • Note: relative volume is only possible in docker-compose and later it is translated into absolute directory (docker container inspect -f '{{ .HostConfig.Binds }}' example_portainer_1)
  • docker-compose stop - stop all running containers
  • docker-compose start - start all stopped containers
  • docker-compose down - stop and remove all containers and then remove web network (owned networks by the compose file)

Note

You can rerun docker-compose up -d every time you change your docker-compose.yml, and it detects the modified services and only rebuild those services.

Services

Traefik

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
  traefik:
    image: traefik:${TRAEFIK_VER:-latest}
    command:
      - "--providers.docker"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.http.address=:80"
      # Dashboard
      - "--api"
      - "--entrypoints.traefik.address=:8080"
    restart: always
    labels:
      # Dashboard
      - "traefik.enable=true"
      - "traefik.http.routers.api.entrypoints=traefik"
      - "traefik.http.routers.api.rule=PathPrefix(`/`)"
      - "traefik.http.routers.api.service=api@internal"
      - "traefik.http.routers.api.middlewares=dashAuth,dashRdir"
      - "traefik.http.middlewares.dashAuth.basicauth.users=admin:$$apr1$$PSIlVhdx$$Np60QsO9D2zneaUjWdaqA0"
      - "traefik.http.middlewares.dashRdir.redirectregex.regex=^(http://[^:/]+(:\\d+)?)(/|/dashboard)$$"
      - "traefik.http.middlewares.dashRdir.redirectregex.replacement=$${1}/dashboard/"
    ports:
      - 80:80
      - 8080:8080
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - net
  • Lines 8 enables dashboard and lines 9 create an entrypoint for the dashboard on port 8080
  • Lines 13-20 configure dashboard with basic-auth and redirection to /dashbiard
    • Line 18 defines basic-auth middleware with user admin (REF)
      • Using echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g command to generate the expression
      • Note: htpasswd is installed via apache2-utils package on Debian
    • Lines 19 and 20 enables the redirection to /dashboard
      • In line 20, the ${1} refers to the group defined in the regex in previous line

Portainer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  portainer:
    image: portainer/portainer:${PORTAINER_VER:-latest}
    command: -H unix:///var/run/docker.sock
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.portainer.rule=PathPrefix(`/portainer`)"
      - "traefik.http.routers.portainer.middlewares=portainerRedir,portainerPStrip"
      - "traefik.http.middlewares.portainerPStrip.stripprefix.prefixes=/portainer"
      - "traefik.http.middlewares.portainerRedir.redirectregex.regex=^(.*)/portainer$$"
      - "traefik.http.middlewares.portainerRedir.redirectregex.replacement=$${1}/portainer/"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ${VOL_BASE_DIR:-.}/Portainer:/data
    networks:
      - net

Kafka

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
version: '3.6'

services:
  zookeeper:
    image: confluentinc/cp-zookeeper:${CONFLUENT_VER:-latest}
    restart: always
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
    volumes:
      - ${VOL_BASE_DIR:-.}/zookeeper/data:/var/lib/zookeeper/data
      - ${VOL_BASE_DIR:-.}/zookeeper/log:/var/lib/zookeeper/log
    networks:
      - net

  kafka:
    image: confluentinc/cp-kafka:${CONFLUENT_VER:-latest}
    restart: always
    depends_on:
      - zookeeper
    environment:
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

      # Multi-Net Config
      KAFKA_LISTENERS: INSIDE://:9092,OUTSIDE://:29092
      KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9092,OUTSIDE://localhost:29092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE

      # All-Same-Net Simple Config
      # KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
    volumes:
      - ${VOL_BASE_DIR:-.}/kafka/data:/var/lib/kafka/data
    ports:
      - 29092:29092
    networks:
      - net

  kafdrop:
    image: obsidiandynamics/kafdrop:${KAFDROP_VER:-latest}
    restart: always
    depends_on:
      - kafka
    environment:
      JVM_OPTS: "-Xms16M -Xmx48M"
      KAFKA_BROKERCONNECT: kafka:9092
      SERVER_PORT: 9090
      SERVER_SERVLET_CONTEXTPATH: "/"
    ports:
      - 9090:9090
    networks:
      - net
  • Kafka Networking Config (REF):
    • All containers attached to net network can connect to kafka server via port 9092 through INSIDE://:9092 listener advertised by INSIDE://kafka:9092, such as kafdrop
    • Other clients on the host (localhost) can connect to kafka server via port 29092 through OUTSIDE://:29092 listener advertised by OUTSIDE://localhost:29092
  • docker-compose exec kafka bash - Check Kafka Server
    • Topic commands
      • kafka-topics --zookeeper zookeeper:2181 --list
      • kafka-topics --zookeeper zookeeper:2181 --create --partitions 3 --replication-factor 1 --if-not-exists --topic bar
      • kafka-topics --zookeeper zookeeper:2181 --describe --topic bar
    • Messaging commands
      • seq -f "MSG_ID: %03g" 20 | kafka-console-producer --broker-list localhost:9092 --topic bar && echo 'Messages Are Sent'
      • kafka-console-consumer --bootstrap-server localhost:9092 --from-beginning --topic bar --max-messages 10
      • Note: the order of messages are based on partitions
  • For Clustered Deployment on Docker check [REF]

Redis

1
2
3
4
5
6
7
8
9
10
  redis:
    image: redis:${REDIS_VER:-latest}
    restart: always
    command: redis-server --requirepass ${REDIS_PASSWD:-redis} --appendonly yes
    ports:
      - 6379:6379
    volumes:
      - ${VOL_BASE_DIR:-.}/redis:/data
    networks:
      - net
  • docker-compose exec redis redis-cli -a ReDis - Interactive Redis CLI
  • Syntax: redis-cli [-h HOST] [-p PORT] [-a PASSWORD] -Commands
    • ping
    • keys * - return all keys
    • type KEY - check type of value
    • del KEY
    • Getting values - Redis supports multiple types of data (REF)
      • get KEY - string
      • smembers KEY - set
      • lrange KEY START END - list
  • docker-compose exec redis redis-cli -a ReDis --stat [-i INTERVAL] - Continuous stats mode

MySQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  mysql:
    image: mysql:${MYSQL_VER:-latest}
    restart: always
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWS:-root}
    volumes:
      - ${VOL_BASE_DIR:-.}/mysql:/var/lib/mysql
    networks:
      - net

  adminer:
    image: adminer:${ADMINER_VER:-latest}
    restart: always
    ports:
      - 8080:8080
    environment:
      ADMINER_DESIGN: ng9
    networks:
      - net
  • What’s New in MySQL 8.0?
  • docker-compose exec mysql -uroot -prOOt - Run MySql CLI
    • Syntax: mysql -uUSER -pPASSWORD (no space!)
    • show databases;
    • create database DB;
    • use DB
    • show tables;
    • desc TABLE;
    • create user USER identified by 'PASSWORD';
    • grant all privileges on DB.* to USER;
  • docker-compose exec mysql mysqldump -uUSER -pPASSWORD DB | grep -v Warning | gzip > DB_$(date +'%Y-%m-%d_%H-%M-%S').sql.gz
  • zcat DB.sql.gz | docker-compose exec -T mysql mysql -uUSER -pPASSWORD DB
    • Note: By default docker-compose exec allocates a tty, so -T disables pseudo-tty allocation.