Introduction
Welcome back and thank you for sticking with me! In a previous post, we wrote our first Docker Compose script which identified and ran the Docker file from our first lesson. We also demonstrated some of the different command line arguments associated with Docker Compose, and differentiated between attached and detached containers. In this tutorial, we’ll build on what we previously learned by incorporating a second container into our Docker Compose file and attaching them both to a network.
In case you’ve landed on this tutorial without visiting the previous segments, we are working in a directory with two files: a dockerfile and a docker-compose.yml file, each of which are shown below:
The Docker file:
FROM alpine:3.14 AS build
LABEL maintainer = "Aaron Pung (5/27/2022)"
LABEL version = "1.0"
LABEL description = "Our first Docker file!"
The Docker Compose file:
version: "3.9"
# Define services
services:
container1:
container_name: first_container
build:
context: .
dockerfile: dockerfile
command: [sleep, "5"]
Adding a second container
One of the powerful aspects of Docker is the ease at which we can add a container to our Docker Compose file. Similar to how we defined our first container as a service, we are able to add a second container in the same manner. For simplicity, we’ll call this container “container2”, and we’ll place it just under container1 using the same syntax and definitions. In fact, for simplicity, we do not even need to make a second Docker file. If we choose, we can simply create a second container exactly identical to the first:
version: "3.9"
# Define services
services:
container1:
container_name: first_container
build:
context: .
dockerfile: dockerfile
command: [sleep, "5"]
container2:
container_name: second_container
build:
context: .
dockerfile: dockerfile
command: [sleep, "5"]
With the two containers defined in the docker-compose.yml file, we can run Docker Compose using the docker-compose up command in the VS Code terminal window. Upon issuing the command, Docker Compose gives us the following response. Note that although we have not explicitly asked Docker to generate a network, it appears a network has been automatically generated…we’ll address this in a moment. We can also see that it has successfully created our first and second containers without error.
Similar to our last lesson, we can then query the state of the containers in Docker Compose using docker-compose ps. Both containers show that they have exited after executing the sleep 5 command detailed in each container service from our YAML file. Interestingly, Docker Compose creates the containers in parallel — that is, it creates first_container and second_container at the same time. We know this because the total compilation time for both containers is roughly five seconds, and not the 10 seconds it would take to run both command: [sleep,"5"] commands for both services.
vscode ➜ /com.docker.devenvironments.code $ docker-compose up
Creating network "comdockerdevenvironmentscode_default" with the default driver
Creating second_container ... done
Creating first_container ... done
Attaching to first_container, second_container
first_container exited with code 0
second_container exited with code 0
vscode ➜ /com.docker.devenvironments.code $ docker-compose ps
Name Command State Ports
-------------------------------------------
first_container sleep 5 Exit 0
second_container sleep 5 Exit 0
We can further demonstrate their parallel creation by running the same up command with the additional detached flag (-d) flag, and querying the state of the containers. By the way, we can also simplify our command sequences by running both commands (our docker-compose up -d command and docker-compose ps) on one line, separated by a semicolon as shown below. The resulting query shows both containers up and running, and both are executing the sleep command we’ve issued to them.
vscode ➜ /com.docker.devenvironments.code $ docker-compose up -d; docker-compose ps
Creating network "comdockerdevenvironmentscode_default" with the default driver
Creating first_container ... done
Creating second_container ... done
Name Command State Ports
------------------------------------------
first_container sleep 5 Up
second_container sleep 5 Up
Cleaning up: Orphans and Pruning
In the previous sections, we saw that Docker Compose gifted us a network (albeit one with a complicated name!) without us even asking. So let’s clear out our networks and clean up our systems using two easy commands:
docker-compose down --remove-orphans
docker-compose system prune
Although we’ve seen the down command before, notice the added flag commanding Docker to remove orphans. This flag comes in handy if we happen to have created containers in a previous instance of running docker-compose up
, but had since removed the containers. This is also why I prefer to run docker-compose down in between each run, so that unused and closed containers are removed. Otherwise, containers from one run might still hang around since we have not explicitly asked Docker to get rid of them. In this case, though, Docker lets us know about the extra containers via a Warning as shown below. The only caveat, though, is that if the old container is stopped, the –remove-orphans flag does nothing.
vscode ➜ /com.docker.devenvironments.code $ docker-compose up -d
WARNING: Found orphan containers (second_container) for this project. If you removed or renamed this service in your compose file, you can run this command with the --remove-orphans flag to clean it up.
Starting first_container ... done
Similarly, the command to prune the Docker Compose system is a single command that removes all stopped containers, unused networks, and dangling images and build caches. Luckily, Docker makes us choose a “Yes” or “No” option to avoid doing anything on accident. Coincidentally, this confirmation is not something we can get around by adding a -y flag to the end of our prune command since it is not one of the (two) options Docker recognizes with the prune command.
vscode ➜ /com.docker.devenvironments.code $ docker system prune
WARNING! This will remove:
- all stopped containers
- all networks not used by at least one container
- all dangling images
- all dangling build cache
Are you sure you want to continue? [y/N] y
Deleted Containers:
de471b1a0cd0dbd81d69906cdba2eda1239cf948f63c42347ba0168aea1ad0f3
a55ed7df71b3bfe73526af7f395e377b030e3280d1fec658ead47914f1345883
Deleted Networks:
comdockerdevenvironmentscode_default
Total reclaimed space: 0B
After confirming our command choice, Docker explicitly shows the alphanumeric SHA keys associated with each deleted container, network, image, and build cache. At the end, Docker also returns the amount of reclaimed memory; depending on the size of your containers volumes, etc. this could be a very large number!
Adding a [local] network
With all of our containers and networks removed, let’s look at the networks natively offered by Docker using the docker network ls
command. With no containers running and Docker Compose not spun up, there are three local networks available, each with a separate name and driver:
vscode ➜ /com.docker.devenvironments.code $ docker network ls
NETWORK ID NAME DRIVER SCOPE
52f7c50bba01 bridge bridge local
98aaea3435ef host host local
64e797fc8d4f none null local
Revisiting our Docker Compose file, we can explicitly define a network under a separate section appropriately titled networks. Furthermore, we can ensure that each container is connected to the same network we’ve defined by additionally defining the new network under each service. This assigns the container to a specific network.
version: "3.9"
# Define services
services:
container1:
container_name: first_container
build:
context: .
dockerfile: dockerfile
command: [sleep, "5"]
networks:
- mynetwork
container2:
container_name: second_container
build:
context: .
dockerfile: dockerfile
command: [sleep, "5"]
networks:
- mynetwork
networks:
mynetwork:
name: mynetwork
Now when we spin up the Docker Compose file, we see that a network is created, named based on the name (“mynetwork) we’ve defined. We can also query Docker as to the existing networks. When we do so, we see that another network, named “mynetwork”, has been created, and is assigned a bridge driver within the local scope.
vscode ➜ /com.docker.devenvironments.code $ docker-compose up -d
Creating network "mynetwork" with the default driver
Creating second_container ... done
Creating first_container ... done
vscode ➜ /com.docker.devenvironments.code $ docker network ls
NETWORK ID NAME DRIVER SCOPE
52f7c50bba01 bridge bridge local
98aaea3435ef host host local
62cadd5a5573 mynetwork bridge local
64e797fc8d4f none null local
Great! But we still need to prove that our containers are attached to the same network. By default, we can either inspect a network by its Network ID or by its Name. In our case, the name is easier. We can inspect a specific network using the convention: docker network inspect <network name>
, which will return all information for the specified network including the running containers attached to it. The information about the network we’ve specified now shows that both first_container
and second_container
are attached to the network while they are running.
vscode ➜ /com.docker.devenvironments.code $ docker network inspect mynetwork
[
{
"Name": "mynetwork",
"Id": "2e59a5c8014ead9bf6252bfbf5ca1ca27b56979e97604ecc68e5beaaecc271ff",
"Created": "2022-06-06T16:46:45.5269557Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.30.0.0/16",
"Gateway": "172.30.0.1"
}
]
},
"Internal": false,
"Attachable": true,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"908a17f78f7c1dfbcc8ad9f0b4db16699149c644a21b747e6c69cb96c8d88d62": {
"Name": "first_container",
"EndpointID": "d523adad81db5619167c25885b4da109c538cb06b0832e8c20569b9e6f07ab5f",
"MacAddress": "02:42:ac:1e:00:03",
"IPv4Address": "172.30.0.3/16",
"IPv6Address": ""
},
"d072b34d121e39495c76fc4c42076c3c62eec32edf293068b3c9acde14dbd271": {
"Name": "second_container",
"EndpointID": "64ae96a008adc0f763325a2865663025fbc2f5ca102725c2469351cdcc38e898",
"MacAddress": "02:42:ac:1e:00:02",
"IPv4Address": "172.30.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {
"com.docker.compose.network": "mynetwork",
"com.docker.compose.project": "comdockerdevenvironmentscode",
"com.docker.compose.version": "1.29.2"
}
}
]
Conclusion
In this lesson, we’ve shown how to modify the Docker Compose file to add a second container by specifying a second service. In this case, the second container is identical to the first since the service description and the Docker file used to identify each container were the same. After demonstrating that both containers were running, we demonstrated creating a network and gave it a customized name. Lastly, we briefly demonstrated that both containers exist on the same network, allowing them to share information.
Although we could demonstrate how to share information between containers over the network, this is a topic left for another lesson. Instead, the next tutorial will focus on another powerful aspect of multi-container communication: sharing a volume! In the meantime, thank you for dropping by! I hope you found this content helpful. If so, please feel free to subscribe!
Get new content delivered directly to your inbox.