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.
Last updated: December 18, 2024
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.
Docker volumes are of two types:
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/.
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:
Additionally, the local-persist plugin is useful for running stateless containers across multiple hosts.
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.
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.
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.
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.
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.
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.