Docker Demos — Getting started with Docker Compose

Introduction

In our last post, we learned our way around Microsoft VS Code and built our first container using our first Docker file! But we can take one more step into abstraction by creating a file to build and run our container for us. Thankfully, Docker Desktop comes with a tool to do this, called Docker Compose.

Keep in mind, though, that we will now be dealing with commands for Docker and Docker Compose. As we previously saw, commands to Docker will be issued using the docker prefix, while calls to Docker Compose will use the docker compose (or docker-compose) command. Before we get into that, let’s set up our Docker Compose file.

Setting up a Docker Compose file

Docker likes to keep things simple, and that’s a good thing. Similar to how we named our first Docker file “dockerfile”, we can apply a similar naming convention to our Docker Compose file. To generate a new Docker Compose file, click in the VS Code Explorer and click “New File”. In the box, enter docker-compose.yml. to generate a .yml file — the file extension for YAML (Yet Another Machine Language) file. You can see from the screenshot below that Docker again auto-recognizes the filetype, and gives us a pretty pink whale icon!

While the Docker file we made will generate our Alpine (version 3.14) container, the docker-compose file will actually be driving the dockerfile to create the container. Similar to our dockerfile, we need a standard first line to kick off the docker-compose file. In our dockerfile, this was the FROM command, where we specified which image we wanted to base our container on. In the docker-compose file, however, this will be the version of docker-compose we want to use. For this demonstration, let’s use version 3.9.

We then want to start defining the services that we plan to run. By “services”, I mean which dockerfile we want to execute, and all of their associated information (the name we’ll refer to them by, whether they’re on a network, where they store information, et cetera). Our first service will only run our Docker file, which we’ve named “dockerfile”.

version: "3.9"

# Define services
services:
    container1:
        container_name: first_container
        build:
            context: .
            dockerfile: dockerfile

We’ll begin by defining by defining a service, container1, and we’ll give it a name we can use later. Let’s call the service “first_container”. The next line, build, contains information about where our Docker file is located — context:. tells Docker to look in our current director, and dockerfile: dockerfile defines the name of the Docker file it should look for in the current directory. In this case, our file is called “dockerfile”.

Running a Docker Compose file

We can run this file within our terminal by using the docker-compose prefix:

docker-compose up --build

Here, the up command tells Docker to spin up the Docker Compose file, and the –build flag tells Docker Compose we want to build all of the containers defined within our services section.

vscode ➜ /com.docker.devenvironments.code $ docker-compose up --build
Creating network "comdockerdevenvironmentscode_default" with the default driver
Building container1
[+] Building 4.9s (5/5) FINISHED                                                                                                                                                                                                              
 => [internal] load build definition from dockerfile                                                                                                                                                                                     0.0s
 => => transferring dockerfile: 182B                                                                                                                                                                                                     0.0s
 => [internal] load .dockerignore                                                                                                                                                                                                        0.0s
 => => transferring context: 2B                                                                                                                                                                                                          0.0s
 => [internal] load metadata for docker.io/library/alpine:3.14                                                                                                                                                                           4.8s
 => CACHED [1/1] FROM docker.io/library/alpine:3.14@sha256:06b5d462c92fc39303e6363c65e074559f8d6b1363250027ed5053557e3398c5                                                                                                              0.0s
 => exporting to image                                                                                                                                                                                                                   0.0s
 => => exporting layers                                                                                                                                                                                                                  0.0s
 => => writing image sha256:bfb5505f953ee93520f53ea44c322f78e353f9dd4dff343bd9c26d58a12f38ea                                                                                                                                             0.0s
 => => naming to docker.io/library/comdockerdevenvironmentscode_container1                                                                                                                                                               0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
Creating first_container ... done
Attaching to first_container
first_container exited with code 0

So what happened here? When we ran our command, Docker Compose began iterating through the services we specified, and built each container in turn. The blue block of text is simply the output of Docker Compose building our container.

Similar to when we previously built our image using dockerfile, there are additional arguments we can add to specify the behavior of how Docker builds our container. Some of these commands are listed below:

  • --no-cache: (Re)build each container without using cached information.
  • --detach: Run containers in the background and print new container names.
  • --no-start: Build the containers, but do not run them after they are built.

We know why the first option is helpful, as we’ve previously discussed. But why does it matter if we run Docker Compose in detached mode? The command we just issued to Docker effectively says, “Spin up the Docker Compose file, build each container, and after each container runs, the process is finished — shut it down.”

Let’s try re-running the docker-compose command again, this time using the –detached (or -d) flag. Additionally, we’ll go ahead an remove the --build flag, since our containers are already built. We’ll also add a sleep command, telling Docker Compose to wait 20 seconds before completing the service build.

version: "3.9"

# Define services
services:
    container1:
        container_name: first_container
        build:
            context: .
            dockerfile: dockerfile
        command: [sleep, "20"]

Now, let’s rerunning the Docker Compose file. If we run the file with docker-compose up, the container build takes just over 20 seconds — this is mostly due to our sleep command! However, the container is built, Docker Compose attaches to the container, and the sleep command finishes. At this point, the container closes because all processes within the container have completed.

vscode ➜ /com.docker.devenvironments.code $ docker-compose up
Creating network "comdockerdevenvironmentscode_default" with the default driver
Creating first_container ... done
Attaching to first_container
first_container exited with code 0

If we run docker-compose ps immediately after completion of the build, we see the container status is now closed:

vscode ➜ /com.docker.devenvironments.code $ docker-compose ps
     Name         Command    State    Ports
-------------------------------------------
first_container   sleep 20   Exit 0   

We’ll run docker-compose down to get rid of all the containers we’ve created, then spin up the Docker Compose file in detached mode (docker-compose up -d). This time, though, the container is created and run in the background; this means the docker-compose command completes in much less time, and we are returned to the command prompt while the container completes its sleep process.

vscode ➜ /com.docker.devenvironments.code $ docker-compose up -d
Starting first_container ... done

Similarly, we can run the ps command to get the container status while the sleep command is running in the background. Doing so demonstrates that the container is indeed still up, active, and running the sleep process.

vscode ➜ /com.docker.devenvironments.code $ docker-compose ps
     Name         Command    State   Ports
------------------------------------------
first_container   sleep 20   Up    

By the way! Keep in mind that it is not necessary to spin down your Docker Compose file (docker-compose down) before rerunning it — down just creates a quick means to remove any networks and containers you may have made. If you simply re-run the up command, Docker will ingest any changes you’ve made to the dockerfile and/or docker-compose file and rebuild the container without removing the network and existing containers:

vscode ➜ /com.docker.devenvironments.code $ docker-compose up
Recreating first_container ... done
Attaching to first_container
first_container exited with code 0

Conclusion

In this tutorial, we’ve built a new Docker Compose file to wrap around our existing Docker file. Using docker-compose commands, we’ve run our newly created file and demonstrated how the --build and --detached (or -d) flags can be used to tailor our debugging process.

In the next tutorial, we’ll continue to advance our use of the Docker Compose file, introducing more advanced features like networks, volumes, and other input arguments. I hope you find this content helpful — if so, please feel free to subscribe!

Get new content delivered directly to your inbox.

(Header image: Technology wave by liuzishan)

%d bloggers like this: