Docker Demos — Sharing a volume between two containers

Introduction

Welcome back! In our last segment, we discussed attaching two containers to a single network within a Docker Compose file. This entailed defining a network within each service (in turn defining each container), as well as defining a network outside of the services section. This ensured that we defined a network with a memorable name and that each container would attach to the specified network.

For simplicity, let’s restart at a more simple point. We’ll start with one Docker file (named dockerfile):

FROM alpine:3.14 AS build

LABEL maintainer = "Aaron Pung (5/27/2022)"
LABEL version = "1.0"

and one Docker Compose file (named docker-compose.yml):

version: "3.9"

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

    container2:
        container_name: second_container
        build:
            context: .
            dockerfile: dockerfile
        command: [sleep, "1000"]

Note that the sleep command is very long, but this is done to keep the containers open while we explore passing information back and forth between containers.

Creating a shared volume

Similar to when we defined our network, we can run docker volume ls to get an idea of the volumes that currently exist before we define our own (shown below). Although we haven’t defined a specific volume nor asked Docker to generate one, a volume is created for each container whenever that container is run. Keep in mind that when you enter a Docker environment, you are placed inside a container by default, and we use Docker files within that container to define our containers. The latter containers are built by Docker Compose.

For instance, we created an environment in Docker, which was named something goofy (like busy_napier). When we develop in VS Code, we are placed inside the busy_napier container and Docker automatically generates a volume. Running docker volume ls gives us the name of the volume:

# List existing Docker volumes before we generate one
vscode ➜ /com.docker.devenvironments.code $ docker volume ls
DRIVER    VOLUME NAME
local     vsCodeServerVolume-docker_intro-busy_napier

But let’s say we want to define our own volume, something with a bit more catch name. Thanks to the Docker’s emphasis on readability, defining a volume is very similar to defining a network. In a similar manner, we’ll begin by defining a volume within each container, but we’ll also define a volume in a separate volume section. Note that we are also giving the volume a name — let’s choose something like datavol for simplicity.

version: "3.9"

# Define services
services:
    container1:
        container_name: first_container
        build:
            context: .
            dockerfile: dockerfile
        command: [sleep, "1000"]
        # Tie container1 to volume "datavol"
        volumes:
            - datavol:/usr/share/appdata

    container2:
        container_name: second_container
        build:
            context: .
            dockerfile: dockerfile
        command: [sleep, "1000"]
        # Tie container2 to volume "datavol"
        volumes:
            - datavol:/usr/share/appdata

# Create a new volume. Name is "datavol"
volumes:
    datavol:
        name: datavol

Notice that there is one more detail that we included along with the volume definition in each container — the location of where the shared volume is to be stored within the specific container. For instance, container1 has a defined volume as datavol, but the colon maps datavol to the folder /usr/share/appdata within the container1 volume!

To see this in action, let’s go ahead and spin up our Docker compose file. We see that both of our containers have been created, both containers are up and running, and the new volume datavol has been identified.

# Spin up the Docker Compose file
vscode ➜ /com.docker.devenvironments.code $ docker-compose up -d
Creating network "comdockerdevenvironmentscode_default" with the default driver
Creating first_container  ... done
Creating second_container ... done

# Make sure both containers are up and running
vscode ➜ /com.docker.devenvironments.code $ docker-compose ps
      Name          Command     State   Ports
---------------------------------------------
first_container    sleep 1000   Up           
second_container   sleep 1000   Up           

# List all existing Docker volumes
vscode ➜ /com.docker.devenvironments.code $ docker volume ls
DRIVER    VOLUME NAME
local     datavol
local     vsCodeServerVolume-docker_intro-busy_napier

Next, we can inspect datavol. Its details reveal that it was created recently, its name is indeed “datavol”, and the volume was created within a local scope.

# Inspect the newly created volume
vscode ➜ /com.docker.devenvironments.code $ docker volume inspect datavol
[
    {
        "CreatedAt": "2022-06-06T17:55:27Z",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "comdockerdevenvironmentscode",
            "com.docker.compose.version": "1.29.2",
            "com.docker.compose.volume": "datavol"
        },
        "Mountpoint": "/var/lib/docker/volumes/datavol/_data",
        "Name": "datavol",
        "Options": null,
        "Scope": "local"
    }
]

We also mentioned that the volume should be accessible to both containers, and that within each container, the shared volume would reside in the /usr/share/appdata directory. To prove this, let’s peek inside this directory in our first container, container1.

Since the container is running, we can run container1 using the Docker Compose command docker-compose run <container name> sh, where <container name> in our case is “container1”. We can expect the same output from container2, since no files have been generated for either container yet!

vscode ➜ /com.docker.devenvironments.code $ docker-compose run container1 sh
Creating comdockerdevenvironmentscode_container1_run ... done

# Look around in directory
/ # ls
bin    dev    etc    home   lib    media  mnt    opt    proc   root   run    sbin   srv    sys    tmp    usr    var

# Change to shared volume directory
/ cd usr/share/appdata

# Look at files within directory -- no files found.
/usr/share/appdata # ls
/usr/share/appdata # 

Demonstrating volume sharing

To demonstrate that both containers are sharing datavol, let’s go ahead and step inside the other container (container2), and go to the same directory. Within the shared volume directory, we’ll create a file named “dog.png”.

# Check to make sure both containers are still running
vscode ➜ /com.docker.devenvironments.code $ docker-compose ps
      Name          Command     State   Ports
---------------------------------------------
first_container    sleep 1000   Up           
second_container   sleep 1000   Up     

# Enter container2
vscode ➜ /com.docker.devenvironments.code $ docker-compose run container2 sh
Creating comdockerdevenvironmentscode_container2_run ... done

# Enter shared volume directory
/ cd /usr/share/appdata

# Examine files in current directory -- nothing found
/usr/share/appdata # ls
/usr/share/appdata # 

# Create a new file, "dog.png"
/usr/share/appdata # touch dog.png

# Examine files in directory -- new file is found!
/usr/share/appdata # ls
dog.png

# Exit the container
/usr/share/appdata # exit

Okay, so it appears the file we’ve created (“dog.png”) has been successfully created within the shared volume directory of container2! Now we’ll back out of container2, re-enter container1, and see if the file exists on the shared volume.

# Re-enter container1
vscode ➜ /com.docker.devenvironments.code $ docker-compose run container1 sh
Creating comdockerdevenvironmentscode_container1_run ... done

# Enter shared volume directory
/ cd /usr/share/appdata

# Examine files in current directory -- the new file is there!
/usr/share/appdata # ls
dog.png
/usr/share/appdata # 

Cleaning up

Similar to our last tutorial, we may have a need to clean up throughout our trial and error process. Earlier, we used the commands

docker-compose down --remove-orphans
docker-compose system prune

to (a) spin down our Docker Compose file, (b) remove any containers that may still exist, and (c) clean up stopped containers, unused networks, dangling images, and dangling build caches. But this time, we’re dealing with volumes — that’s important, because Docker does not allow you to define a new volume when a volume of the same name already exists (say, from a previous Docker run). These two commands gave us the following outputs:

vscode ➜ /com.docker.devenvironments.code $ docker-compose down
Removing first_container ... done
Removing second_container ... done
Removing network comdockerdevenvironmentscode_default
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:
3b6c13e1cbc74ae4cbf9b2353d14974422b4a4ed81e5dc3e8c02bb143a2c4223
6d0e41081deed799103189bc7ec803e95b63915bff913bbb6d27ef57127f83de

Total reclaimed space: 0B

To fix this issue, we can add the -v tag to our first command. This will also remove the volume that we’ve created within this Docker Compose run. Volume removal based on this flag can be seen in Docker’s return status:

vscode ➜ /com.docker.devenvironments.code $ docker-compose down -v
Removing first_container ... done
Removing second_container ... done
Removing network comdockerdevenvironmentscode_default
Removing volume comdockerdevenvironmentscode_appdata

Conclusion

In this tutorial, we’ve shown how to define a new storage volume and we’ve given it a custom name, “datavol”. We then tied each container to the new volume, and defined the directory in each container where we could access the shared volume. Finally, we entered one of the containers (container2), and created a new file, “dog.png”. After exiting container2, we were able to re-enter container1 to show that the file created within container2 exists on the shared volume in a way that can be seen by both containers.

With this capability in hand, there are many useful routes we can demonstrate in terms of collecting, processing, and sharing data. Until then, thanks again for stopping by! I hope you found this content helpful — if so, feel free to subscribe!

Get new content delivered directly to your inbox.

(Header image: Plexus photo by kjpargeter)

%d bloggers like this: