Docker-compose

Docker-compose: better together

Docker containers are super powerful when isolated, but even more powerful when user together. However, setting up the relationship and managing containers can get very messy very quickly.

To solve that, in a world of microservices, Docker-compose was created.

The docker-compose.yml file

As the extension suggests, docker-compose microservices are defined via a YAML file.

Each service is described by a name and an image, for example the service ubuntu using the container image ubuntu:22.04:

services:
    ubuntu:
        image: ubuntu:22.04

Typically though, you are likely to see port and volume mappings, a container name, restart policy and environment variables used to configure the server. Using the example of the LinuxServer’s nginx image:

services:
  nginx:
    image: lscr.io/linuxserver/nginx:latest
    container_name: nginx
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Etc/UTC
    volumes:
      - ./nginx_data:/config
    ports:
      - 80:80
      - 443:443
    restart: unless-stopped

Running docker-compose services

To create the container and start it, we use the docker-compose up -d command. Since I can’t use those lower range ports, I’m replacing them with 8080 and 8443.

$ docker-compose up -d
Creating network "oran_testbed_docs_default" with the default driver
Pulling nginx (lscr.io/linuxserver/nginx:latest)...
latest: Pulling from linuxserver/nginx
e17344a03a1f: Pull complete
07a0e16f7be1: Pull complete
85b9c0ea220a: Pull complete
109c94cd8876: Pull complete
15d4551eb88e: Pull complete
224d9e9f6d7a: Pull complete
fa68b884f0e7: Pull complete
46af7d502d5c: Pull complete
Digest: sha256:b022f503603da72a66a3d07f142c791257dcc682c7a4749881aecf0dc615b266
Status: Downloaded newer image for lscr.io/linuxserver/nginx:latest
Creating nginx ... done
$ docker container ls
CONTAINER ID   IMAGE                              COMMAND                  CREATED             STATUS             PORTS                                                                            NAMES
1ad2604f8ed2   lscr.io/linuxserver/nginx:latest   "/init"                  14 seconds ago      Up 13 seconds      0.0.0.0:8080->80/tcp, :::8080->80/tcp, 0.0.0.0:8443->443/tcp, :::8443->443/tcp   nginx
2406929926a9   registry:2                         "/entrypoint.sh /etc…"   About an hour ago   Up About an hour   0.0.0.0:5000->5000/tcp, :::5000->5000/tcp

As we can see, the nginx service was created and its associated container was initiated. To connect to the container, we can use good old docker exec -it nginx bash

$ docker exec -it nginx bash
root@1ad2604f8ed2:/# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         214  0.0  0.0  49656 13484 ?        Ss   16:52   0:00 nginx: master process /usr/sbin/nginx
root         215  0.0  0.0 227780 30188 ?        Ss   16:52   0:00 php-fpm: master process (/etc/php82/php-fpm.conf)
abc          245  0.0  0.0  50120  5116 ?        S    16:52   0:00 nginx: worker process
...

If we look at the nginx_data, we are going to see files created by the initial setup of the server.

$ tree ./nginx_data
nginx_data/
├── keys
│   ├── cert.crt
│   └── cert.key
├── log
│   ├── nginx
│   │   ├── access.log
│   │   └── error.log
│   └── php
│       └── error.log
├── nginx
│   ├── dhparams.pem
│   ├── nginx.conf
│   ├── nginx.conf.sample
│   ├── resolver.conf
│   ├── site-confs
│   │   ├── default.conf
│   │   └── default.conf.sample
│   ├── ssl.conf
│   ├── ssl.conf.sample
│   └── worker_processes.conf
├── php
│   ├── php-local.ini
│   └── www2.conf
└── www
    └── index.html

8 directories, 17 files

For security reasons, some of these can be made readonly (logs are a clear exception).

A different way to check out of the services are working is using docker-compose ps.

$ docker-compose ps
Name    Command   State                                      Ports
------------------------------------------------------------------------------------------------------
nginx   /init     Up      0.0.0.0:8443->443/tcp,:::8443->443/tcp, 0.0.0.0:8080->80/tcp,:::8080->80/tcp

Stopping docker-compose services

To stop the services, use docker-compose stop. To stop the services and tear them down, use docker-compose down.

$ docker-compose stop
Stopping nginx ... done
$ docker-compose down
Removing nginx ... done
Removing network oran_testbed_docs_default

Setting up virtual networks between service containers

Each docker container is connected to a virtual network to the host computer. Which means services are isolated from one another, except for exposed mounts or ports.

That is not always desirable, in case the services need to exchange messages between them.

Let’s see the following example with two containers pinging each other.

pinger1_service:
  hostname: pinger1
  image: nginx:latest
  command: bash -c "apt update && apt install -y iputils-ping; ping -c 10 pinger2"

pinger2_service:
  hostname: pinger2
  image: nginx:latest
  command: bash -c "apt update && apt install -y iputils-ping; ping -c 10 pinger1"
$docker-compose up
Creating oran_testbed_docs_pinger1_service_1 ... done
Creating oran_testbed_docs_pinger2_service_1 ... done
Attaching to oran_testbed_docs_pinger1_service_1, oran_testbed_docs_pinger2_service_1
pinger1_service_1  |
pinger1_service_1  | WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
pinger1_service_1  |
pinger2_service_1  |
pinger2_service_1  | WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
pinger2_service_1  |
pinger2_service_1  | Get:1 http://deb.debian.org/debian bookworm InRelease [151 kB]
pinger1_service_1  | Get:1 http://deb.debian.org/debian bookworm InRelease [151 kB]
pinger2_service_1  | Get:2 http://deb.debian.org/debian bookworm-updates InRelease [52.1 kB]
pinger2_service_1  | Get:3 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]
pinger1_service_1  | Get:2 http://deb.debian.org/debian bookworm-updates InRelease [52.1 kB]
pinger1_service_1  | Get:3 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]
pinger2_service_1  | Get:4 http://deb.debian.org/debian bookworm/main amd64 Packages [8780 kB]
pinger1_service_1  | Get:4 http://deb.debian.org/debian bookworm/main amd64 Packages [8780 kB]
pinger2_service_1  | Get:5 http://deb.debian.org/debian bookworm-updates/main amd64 Packages [6668 B]
pinger2_service_1  | Get:6 http://deb.debian.org/debian-security bookworm-security/main amd64 Packages [105 kB]
pinger2_service_1  | Fetched 9143 kB in 2s (5350 kB/s)
pinger2_service_1  | Reading package lists...
pinger2_service_1  | Building dependency tree...
pinger2_service_1  | Reading state information...
pinger2_service_1  | 1 package can be upgraded. Run 'apt list --upgradable' to see it.
...
pinger1_service_1  | Setting up iputils-ping (3:20221126-1) ...
pinger2_service_1  | ping: pinger1: Name or service not known
oran_testbed_docs_pinger2_service_1 exited with code 2
pinger1_service_1  | ping: pinger2: Temporary failure in name resolution
oran_testbed_docs_pinger1_service_1 exited with code 2

As we can see, DNS resolution failed since the services are not part of the same group, so they are completely isolated.

If we create multiple services under the same umbrella, those container are connected to the same network and have automatic DNS name resolution.

services:
  pinger1_service:
    hostname: pinger1
    image: nginx:latest
    command: bash -c "apt update && apt install -y iputils-ping; ping -c 10 pinger2"

  pinger2_service:
    hostname: pinger2
    image: nginx:latest
    command: bash -c "apt update && apt install -y iputils-ping; ping -c 10 pinger1"

After running docker-compose up we get:

$ docker-compose up
Starting oran_testbed_docs_pinger1_service_1 ... done
Starting oran_testbed_docs_pinger2_service_1 ... done
Attaching to oran_testbed_docs_pinger2_service_1, oran_testbed_docs_pinger1_service_1
pinger2_service_1  |
pinger2_service_1  | WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
pinger2_service_1  |
pinger1_service_1  |
pinger1_service_1  | WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
pinger1_service_1  |
pinger1_service_1  | Hit:1 http://deb.debian.org/debian bookworm InRelease
pinger1_service_1  | Hit:2 http://deb.debian.org/debian bookworm-updates InRelease
...
pinger1_service_1  | iputils-ping is already the newest version (3:20221126-1).
pinger1_service_1  | 0 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.
pinger1_service_1  | PING pinger2 (172.22.0.3) 56(84) bytes of data.
pinger1_service_1  | 64 bytes from oran_testbed_docs_pinger2_service_1.oran_testbed_docs_default (172.22.0.3): icmp_seq=1 ttl=64 time=0.063 ms
pinger2_service_1  | iputils-ping is already the newest version (3:20221126-1).
pinger2_service_1  | 0 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.
pinger2_service_1  | PING pinger1 (172.22.0.2) 56(84) bytes of data.
pinger2_service_1  | 64 bytes from oran_testbed_docs_pinger1_service_1.oran_testbed_docs_default (172.22.0.2): icmp_seq=1 ttl=64 time=0.059 ms
pinger1_service_1  | 64 bytes from oran_testbed_docs_pinger2_service_1.oran_testbed_docs_default (172.22.0.3): icmp_seq=2 ttl=64 time=0.074 ms
pinger2_service_1  | 64 bytes from oran_testbed_docs_pinger1_service_1.oran_testbed_docs_default (172.22.0.2): icmp_seq=2 ttl=64 time=0.072 ms
pinger1_service_1  | 64 bytes from oran_testbed_docs_pinger2_service_1.oran_testbed_docs_default (172.22.0.3): icmp_seq=3 ttl=64 time=0.082 ms
pinger2_service_1  | 64 bytes from oran_testbed_docs_pinger1_service_1.oran_testbed_docs_default (172.22.0.2): icmp_seq=3 ttl=64 time=0.076 ms
pinger1_service_1  | 64 bytes from oran_testbed_docs_pinger2_service_1.oran_testbed_docs_default (172.22.0.3): icmp_seq=4 ttl=64 time=0.081 ms
pinger2_service_1  | 64 bytes from oran_testbed_docs_pinger1_service_1.oran_testbed_docs_default (172.22.0.2): icmp_seq=4 ttl=64 time=0.070 ms
pinger1_service_1  | 64 bytes from oran_testbed_docs_pinger2_service_1.oran_testbed_docs_default (172.22.0.3): icmp_seq=5 ttl=64 time=0.075 ms
pinger2_service_1  | 64 bytes from oran_testbed_docs_pinger1_service_1.oran_testbed_docs_default (172.22.0.2): icmp_seq=5 ttl=64 time=0.071 ms
pinger1_service_1  | 64 bytes from oran_testbed_docs_pinger2_service_1.oran_testbed_docs_default (172.22.0.3): icmp_seq=6 ttl=64 time=0.068 ms
pinger2_service_1  | 64 bytes from oran_testbed_docs_pinger1_service_1.oran_testbed_docs_default (172.22.0.2): icmp_seq=6 ttl=64 time=0.045 ms
pinger1_service_1  | 64 bytes from oran_testbed_docs_pinger2_service_1.oran_testbed_docs_default (172.22.0.3): icmp_seq=7 ttl=64 time=0.074 ms
^CGracefully stopping... (press Ctrl+C again to force)
Stopping oran_testbed_docs_pinger2_service_1 ... done
Stopping oran_testbed_docs_pinger1_service_1 ... done

As we can see, now everything works just fine.

However, IPs from the containers may change over time, which is less then ideal for permanently hosted services or services that accept only IP addresses. In this case, we can explicitly create and configure a virtual network.

networks:
  net1:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 101.1.1.0/24

services:
  pinger1_service:
    hostname: pinger1
    image: nginx:latest
    command: bash -c "apt update && apt install -y iputils-ping; ping -c 20 pinger2"
    networks:
      net1:
        ipv4_address: 101.1.1.2

  pinger2_service:
    hostname: pinger2
    image: nginx:latest
    command: bash -c "apt update && apt install -y iputils-ping; ping -c 20 pinger1"
    networks:
      net1:
        ipv4_address: 101.1.1.3

We get the following output:

$ docker-compose up
Recreating oran_testbed_docs_pinger2_service_1 ... done
Recreating oran_testbed_docs_pinger1_service_1 ... done
Attaching to oran_testbed_docs_pinger2_service_1, oran_testbed_docs_pinger1_service_1
...
pinger2_service_1  | PING pinger1 (101.1.1.2) 56(84) bytes of data.
pinger2_service_1  | 64 bytes from oran_testbed_docs_pinger1_service_1.oran_testbed_docs_net1 (101.1.1.2): icmp_seq=1 ttl=64 time=0.073 ms
pinger1_service_1  | Setting up iputils-ping (3:20221126-1) ...
pinger1_service_1  | PING pinger2 (101.1.1.3) 56(84) bytes of data.
pinger1_service_1  | 64 bytes from oran_testbed_docs_pinger2_service_1.oran_testbed_docs_net1 (101.1.1.3): icmp_seq=1 ttl=64 time=0.072 ms
pinger2_service_1  | 64 bytes from oran_testbed_docs_pinger1_service_1.oran_testbed_docs_net1 (101.1.1.2): icmp_seq=2 ttl=64 time=0.072 ms
pinger1_service_1  | 64 bytes from oran_testbed_docs_pinger2_service_1.oran_testbed_docs_net1 (101.1.1.3): icmp_seq=2 ttl=64 time=0.053 ms
pinger2_service_1  | 64 bytes from oran_testbed_docs_pinger1_service_1.oran_testbed_docs_net1 (101.1.1.2): icmp_seq=3 ttl=64 time=0.073 ms

Interdependent containers

A common service requirement is a SQL database service to store data in an ACID manner. The Wordpress CMS, for example, depends on either a SQLite (disk-based database, don’t need a separate host, but isn’t meant for concurrent access), MySQL or PostgreSQL.

The Wordpress container image in Docker Hub contains instructions on how to setup the wordpress service and the required database service. The suggested docker-compose file is as follows:

services:
  wordpress:
    image: wordpress
    restart: always
    ports:
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb
    volumes:
      - wordpress:/var/www/html
  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - db:/var/lib/mysql
volumes:
  wordpress:
  db:

We can see the suggested database user, password and names. These are considered SECRETS, and should never be stored in cleartext files like this.

Note that the mounted volumes are not from the host system, but docker volumes wordpress and db. I don’t recommend ever using these volumes to store services data or settings. But that is my opinion.

Let’s run this to see how it behaves. At the end, we should be greeted by the Wordpress setup wizard on localhost:8080.

docker-compose up
Creating network "oran_testbed_docs_default" with the default driver
Creating volume "oran_testbed_docs_wordpress" with default driver
Creating volume "oran_testbed_docs_db" with default driver
Pulling wordpress (wordpress:)...
latest: Pulling from library/wordpress
b7f91549542c: Pulling fs layer
...
b0e2f5156049: Pull complete
Digest: sha256:824689613b4e7b027d0d36f264a53a159d6c7adcf5250539e56efe2940651e19
Status: Downloaded newer image for wordpress:latest
Pulling db (mysql:5.7)...
5.7: Pulling from library/mysql
11a38aebcb7a: Pulling fs layer
...
ee9043dd2677: Pull complete
Digest: sha256:f566819f2eee3a60cf5ea6c8b7d1bfc9de62e34268bf62dc34870c4fca8a85d1
Status: Downloaded newer image for mysql:5.7
Creating oran_testbed_docs_wordpress_1 ... done
Creating oran_testbed_docs_db_1        ... done
Attaching to oran_testbed_docs_db_1, oran_testbed_docs_wordpress_1
db_1         | 2023-11-28 19:40:55+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.44-1.el7 started.
wordpress_1  | WordPress not found in /var/www/html - copying now...
db_1         | 2023-11-28 19:40:55+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
db_1         | 2023-11-28 19:40:55+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.44-1.el7 started.
wordpress_1  | Complete! WordPress has been successfully copied to /var/www/html
wordpress_1  | No 'wp-config.php' found in /var/www/html, but 'WORDPRESS_...' variables supplied; copying 'wp-config-docker.php' (WORDPRESS_DB_HOST WORDPRESS_DB_NAME WORDPRESS_DB_PASSWORD WORDPRESS_DB_USER)
db_1         | 2023-11-28 19:40:56+00:00 [Note] [Entrypoint]: Initializing database files
db_1         | 2023-11-28T19:40:56.255127Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
wordpress_1  | AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.23.0.2. Set the 'ServerName' directive globally to suppress this message
wordpress_1  | [Tue Nov 28 19:40:56.336165 2023] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.56 (Debian) PHP/8.0.30 configured -- resuming normal operations
wordpress_1  | [Tue Nov 28 19:40:56.336208 2023] [core:notice] [pid 1] AH00094: Command line: 'apache2 -D FOREGROUND'
db_1         | 2023-11-28T19:40:56.602920Z 0 [Warning] InnoDB: New log files created, LSN=45790
db_1         | 2023-11-28T19:40:56.649898Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.
...
db_1         | 2023-11-28T19:40:59.916125Z 0 [Note] mysqld (mysqld 5.7.44) starting as process 125 ...
...
db_1         | 2023-11-28 19:41:02+00:00 [Note] [Entrypoint]: GENERATED ROOT PASSWORD: XZL8xdElzlWrvPEqgZpH6tFMtGg8BcRQ
db_1         | 2023-11-28 19:41:02+00:00 [Note] [Entrypoint]: Creating database exampledb
db_1         | 2023-11-28 19:41:02+00:00 [Note] [Entrypoint]: Creating user exampleuser
db_1         | 2023-11-28 19:41:02+00:00 [Note] [Entrypoint]: Giving user exampleuser access to schema exampledb
db_1         |
db_1         | 2023-11-28 19:41:02+00:00 [Note] [Entrypoint]: Stopping temporary server
...
db_1         | 2023-11-28 19:41:04+00:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up.
db_1         |
db_1         | 2023-11-28T19:41:04.734809Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
db_1         | 2023-11-28T19:41:04.736710Z 0 [Note] mysqld (mysqld 5.7.44) starting as process 1 ...
db_1         | 2023-11-28T19:41:04.817339Z 0 [Note] Server hostname (bind-address): '*'; port: 3306
db_1         | 2023-11-28T19:41:04.817392Z 0 [Note] IPv6 is available.
db_1         | 2023-11-28T19:41:04.817414Z 0 [Note]   - '::' resolves to '::';
db_1         | 2023-11-28T19:41:04.817437Z 0 [Note] Server socket created on IP: '::'.
db_1         | 2023-11-28T19:41:04.818503Z 0 [Warning] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
db_1         | 2023-11-28T19:41:04.826933Z 0 [Note] Event Scheduler: Loaded 0 events
db_1         | 2023-11-28T19:41:04.827130Z 0 [Note] mysqld: ready for connections.
db_1         | Version: '5.7.44'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
wordpress_1  | 172.23.0.1 - - [28/Nov/2023:19:41:11 +0000] "GET / HTTP/1.1" 302 405 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
...

We can see that both services were started and that wordpress connected with the MySQL database.

../_images/wordpress-wizard.png

After registering the wordpress site as umsite and the user as umusuario via the online wizard, we check the database. We can do this using docker exec -it parentFolderName_containerName_num, as shown below.

$ docker exec -it oran_testbed_docs_db_1 bash
bash-4.2# mysql --user=exampleuser --password=examplepass exampledb
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 10
Server version: 5.7.44 MySQL Community Server (GPL)

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> SHOW TABLES;
+-----------------------+
| Tables_in_exampledb   |
+-----------------------+
| wp_commentmeta        |
| wp_comments           |
| wp_links              |
| wp_options            |
| wp_postmeta           |
| wp_posts              |
| wp_term_relationships |
| wp_term_taxonomy      |
| wp_termmeta           |
| wp_terms              |
| wp_usermeta           |
| wp_users              |
+-----------------------+
12 rows in set (0.00 sec)

mysql> SELECT * FROM wp_users;
+----+------------+------------------------------------+---------------+----------------------+-----------------------+---------------------+---------------------+-------------+--------------+
| ID | user_login | user_pass                          | user_nicename | user_email           | user_url              | user_registered     | user_activation_key | user_status | display_name |
+----+------------+------------------------------------+---------------+----------------------+-----------------------+---------------------+---------------------+-------------+--------------+
|  1 | umusuario  | $P$B3A4XjATLk1HuMksgqvM9S6gdNeiUQ0 | umusuario     | emailfalso@gmail.com | http://localhost:8080 | 2023-11-28 19:55:43 |                     |           0 | umusuario    |
+----+------------+------------------------------------+---------------+----------------------+-----------------------+---------------------+---------------------+-------------+--------------+
1 row in set (0.00 sec)

mysql>EXIT;
Bye
bash-4.2# exit

As shown above, we have updated the database with the new user via the Wordpress wizard. After tearing down the setup, those two volumes for wordpress and db files will be kept.

$ docker-compose down
Removing oran_testbed_docs_db_1        ... done
Removing oran_testbed_docs_wordpress_1 ... done
Removing network oran_testbed_docs_default
$ docker volume ls
DRIVER    VOLUME NAME
local     oran_testbed_docs_db
local     oran_testbed_docs_wordpress

Those volumes can be mounted by containers for inspection/data extraction.

$ docker run -it -v oran_testbed_docs_db:/db ubuntu:23.04
Unable to find image 'ubuntu:23.04' locally
23.04: Pulling from library/ubuntu
f93f952dad40: Pull complete
Digest: sha256:51e70689b125fcc2e800f5efb7ba465dee85ede9da9c268ff5599053c7e52b77
Status: Downloaded newer image for ubuntu:23.04
root@08ebebd718d8:/# ls /db/
auto.cnf    ca.pem           client-key.pem  ib_buffer_pool  ib_logfile1  mysql       performance_schema  public_key.pem   server-key.pem
ca-key.pem  client-cert.pem  exampledb       ib_logfile0     ibdata1      mysql.sock  private_key.pem     server-cert.pem  sys

And can also be deleted if no container is currently attached to them. Which is the reason I don’t recommend using them in the first place.

$ docker container prune
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Deleted Containers:
08ebebd718d8b20e40848c11e1dad663022e9f3dc633eb105f24d7b165b0ba47

Total reclaimed space: 13B
$ docker volume prune -a
WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Volumes:
oran_testbed_docs_wordpress
oran_testbed_docs_db

Total reclaimed space: 278.3MB

Containers that require some startup ordering must explicitly list other containers as required, as specified in the compose-file documentation.

Servers are expected to retry connections to ensure out-of-order initialization does not prevent the services from starting.