Baeldung Pro – Ops – NPI EA (cat = Baeldung on Ops)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

Partner – Orkes – NPI EA (cat=Kubernetes)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

1. Introduction

Docker volumes provide a mechanism to store and maintain data independently of a container’s lifecycle. By default, data stored inside a Docker container is ephemeral. However, volumes ensure data survives container restarts, updates, or crashes.

In this tutorial, we’ll explore creating a volume in a specific directory with Docker.

2. How Docker Volumes Work

Docker volumes are of two types:

  1. Docker managed volumes
  2. Bind mounts

Bind mount volumes use any user-specified directory or file on the host operating system. On the other hand, managed volumes use locations created by the Docker daemon in a space called Docker managed space.

Managed volumes can be either named or unnamed volumes. Named volumes are volumes created by specifying the volume name with the docker volume create command:

$ docker volume create <volume_name>

While unnamed volumes are volumes created and attached to a container by Docker using the -v flag with the docker run command:

$ docker run -v /some/path/in/container <image_name>

However, unlike unnamed volumes, which are automatically removed when the container is deleted, named volumes are persistent. While this is a good feature, we can’t specify a directory of choice. Docker places the named volume in a location it fully manages, which we can confirm using the docker inspect command:

$ docker inspect <named_volume>
[
    {
        "CreatedAt": "2024-11-10T09:43:31Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/<named_volume>/_data",
        "Name": "test-demo",
        "Options": null,
        "Scope": "local"
    }
]

The output above shows that the volume’s mountpoint is located at /var/lib/docker/volumes/.

3. Using Named Volumes With the local-persist Plugin

In the previous section, we saw that named volumes, although persistent in nature, cannot be created in a specific user-managed directory. However, the local-persist plugin can overcome this blocker.

The local-persist plugin is an open-source Docker plugin that allows users to create named local volumes that persist in specific locations. The local-persist volume plugin has several advantages over the default local driver:

  • Removing the volume does not delete the data
  • If we add a volume to a local-persist volume, it will still exist after removing and recreating the driver

Additionally, the local-persist plugin is useful for running stateless containers across multiple hosts.

3.1. Installing the local-persist Plugin

To install the plugin on Linux machines, let’s begin by downloading the latest binary from their releases page:

$ wget https://github.com/MatchbookLab/local-persist/releases/download/v1.3.0/local-persist-linux-amd64

Then, rename the binary to docker-volume-local-persist and make it an executable:

$ mv local-persist-linux-amd64 docker-volume-local-persist
$ chmod +x docker-volume-local-persist

Next, we move it to /usr/bin/ the directory:

$ sudo mv docker-volume-local-persist /usr/bin/

This makes it enough to run at this point. However, to make it start with Docker, let’s create a service configuration file named docker-volume-local-persist.service in /etc/systemd/system/ using any editor:

[Unit]
Description=docker-volume-local-persist
Before=docker.service
Wants=docker.service

[Service]
TimeoutStartSec=0
ExecStart=/usr/bin/docker-volume-local-persist

[Install]
WantedBy=multi-user.target

The above systemd unit file ensures the docker-volume-local-persist plugin is always running and ready for use by Docker containers without having to manually start it.

Let’s reload the daemon config and start the service:

$ sudo systemctl daemon-reload
$ sudo systemctl enable docker-volume-local-persist
$ sudo systemctl start docker-volume-local-persist

Hence, we’ve successfully installed the local-persist plugin.

macOS and Windows machines do not support native Docker plugins. However, the maintainers made provisions to support running the plugin from a container:

$ docker run -d \
-v /run/docker/plugins/:/run/docker/plugins/ \
-v /path/to/store/json/for/restart/:/var/lib/docker/plugin-data/ \
-v /path/to/store/data:/path/to/store/data \
cwspear/docker-local-persist-volume-plugin

Hence, the plugin is compatible with Linux, macOS, and Windows machines. Although the plugin’s last release was in 2016, we tested and found it compatible with Docker Engine version 27.3.1.

3.2. Defining the Volume

Let’s create a volume named persist-volume using the local-persist plugin:

$ docker volume create \
-d local-persist \
-o mountpoint=/data/persist \
--name=persist-volume
persist-volume

The above command creates a Docker volume using the local-persist plugin as the driver. It sets the /data/persist directory as the mountpoint. We can confirm the creation volume with the docker volume ls command:

$ docker volume ls
DRIVER          VOLUME NAME
local-persist   persist-volume

Additionally, let’s confirm the mountpoint location and driver details using the inspect command:

$ docker inspect persist-volume
[
    {
        "Driver": "local-persist",
        "Labels": null,
        "Mountpoint": "/data/persist",
        "Name": "persist-volume",
        "Options": {
            "mountpoint": "/data/persist"
        },
        "Scope": "local"
    }
]

As seen above, the persist-volume was created with the correct mountpoint and local-persist driver.

3.3. Verifying Volume Persistence

We can attach a volume using the -v flag or –mount syntax:

# Using -v syntax
docker run -v <volume>:/path/in/container <image_name>

# Using --mount syntax 
docker run --mount source=<volume>,target=/path/in/container <image_name>

Let’s attach the created volume to a container and run a basic file persistence test. In this test, we start a container using an alpine image named test1 and mount the volume to /data:

$ docker run -it --name test1 \
-v persist-volume:/data \
alpine:latest \
/bin/sh
/ #

Inside this container, we write “Hello from container 1” to a file named test.txt and exit the container:

# echo "Hello from container 1" > /data/test.txt
# exit

On exit, we can now remove the container using the rm command:

$ docker rm test1

We then start a new container named test2, again mounting the same volume to /data:

$ docker run -it --name test2 \
-v persist-volume:/data \
alpine:latest \
/bin/sh
/ #

When we view the contents of the test.txt file using the cat command, we see that “Hello from container 1” persists:

/ # cat /data/test.txt
/ # Hello from container 1

This demonstrates that data survives container removal and remains accessible to a new container using the same volume. We can exit the container and delete it.

Additionally, we can delete the volume and still find the saved data in the specified mountpoint on the host:

$ docker volume rm persist-volume
persist-volume
$ ls /data/persist/
test.txt

Hence, creating a new volume and attaching it to the /data/persist mountpoint preserves the existing data. To test this, let’s create a new volume named persist-volume2 and use /data/persist as the mountpoint:

$ docker volume create \
-d local-persist \
-o mountpoint=/data/persist \
--name=persist-volume2
persist-volume2

Then, attach it to a container and verify the presence of the test.txt file:

$ docker run -it \
-v persist-volume:/data \
alpine:latest \
/bin/sh
/ # cat /data/test.txt
Hello from container 1

As seen above, the test.txt file is present. Hence, the local-persist plugin creates volumes that persist in a specific directory.

4. Using Bind Mounts

Bind mounts map a host filesystem directory to a container. They have limited functionality compared to named volumes. For example, volumes are easier to back up or migrate than bind mounts.

Regardless, they’re a much safer and recommended option for creating volumes in a specific directory, as they don’t require any extra installations.

4.1. Verifying Persistence in Bind Mounts

Similarly, we can attach a bind mount to a container using either the -v flag or the –mount syntax:

# Using -v syntax
docker run -v /data/<bind_mount>:/path/in/container <image_name>

# Using --mount syntax
docker run --mount type=bind,source=/data/<bind-mount>,target=/path/in/container <image_name>

To demonstrate how a bind mount persists data in a specific directory, let’s run a basic file persistence demo. We start by creating a directory named bind-demo in the /data directory and add initial content to a text file:

$ mkdir -p /data/bind-demo
$ echo "Initial host content" > /data/bind-demo/test.txt

Then, we start an Alpine container named bind1 and mount the bind-demo directory to /app inside the container:

$ docker run -it --name bind1 \
 --mount type=bind,source=/data/bind-demo,target=/app \
 alpine:latest /bin/sh
/ # ls /app/
test.txt

From the output above, we can confirm the test.txt file is also present in the container. Inside the container, let’s append new text to the file:

/ # echo "Added from container 1" >> /app/test.txt

After exiting the container, we check the host directory (/data/bind-demo) and find both original and new content:

$ cat /data/bind-demo/test.txt
Initial host content
Added from container 1

Additionally, multiple containers can share the same bind mount. Let’s demonstrate by starting a new container named bind2 and mount the bind-demo directory to it:

$ docker run -it --name bind2 \
--mount type=bind,source=/data/bind-demo,target=/app \
alpine:latest /bin/sh

Then, check the contents of the test.txt file in the app directory:

/ # cat /app/test.txt
Initial host content
Added from container 1

Based on the output, we see that bind mounts can persistently share data between the host-specified directory and different containers.

5. Conclusion

In this article, we discussed methods of creating volumes that persist in a specific directory in Docker using the local-persist plugin and bind mounts.

While the local-persist plugin is a good option, bind mounts offer a direct approach as they create an immediate connection between the host filesystems and container storage.