Understanding Volumes
7.1. Introduction to Docker Volumes​
Well, volumes are nothing more than directories external to the container, which are mounted directly into it, and thus bypass its filesystem, meaning they don't follow that layer pattern we discussed. Disappointed? Good, that's a sign it's quite simple and you won't have trouble understanding. :)
The main function of a volume is to persist data. Unlike the container's filesystem, which is volatile and all information written to it is lost when the container dies, when you write to a volume that data remains there, regardless of the container's state.
There are some particularities between volumes and containers worth mentioning:
-
The volume is initialized when the container is created.
-
If there is already data in the directory you are mounting as a volume, that is, if the directory already exists and is "populated" in the base image, that data will be copied to the volume.
-
A volume can be reused and shared among containers.
-
Changes to a volume are made directly to the volume.
-
Changes to a volume won't go with the image when you make a copy or snapshot of a container.
-
Volumes continue to exist even if you delete the container.
That said, enough talk. Let's learn how to add a volume to a container.
First, let's see how it works the old way, which is still supported, but isn't elegant. :)
This method is widely used when you want to mount a specific directory from the host inside the container. This is problematic when working in a cluster, since we would have to ensure this directory is created on all hosts in the cluster. Not ideal.
However, we can learn how it works and use it at some point if necessary. To avoid errors, first create the "/volume" directory on your machine.
root@linuxtips:~# mkdir /volume
root@linuxtips:~# docker container run -ti --mount type=bind,src=/volume,dst=/volume ubuntu
root@7db02e999bf2:/# df -h
Filesystem Size Used Avail Use% Mounted on
none 13G 6.8G 5.3G 57% /
tmpfs 999M 0 999M 0% /dev
tmpfs 999M 0 999M 0% /sys/fs/cgroup
/dev/mapper/ubuntu--vg-root 13G 6.8G 5.3G 57% /volume
shm 64M 0 64M 0% /dev/shm
root@7db02e999bf2:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var volume
root@7db02e999bf2:/#
In the previous example, we learned about a new parameter for the "docker container run" command, the "--mount".
The "--mount" parameter is responsible for indicating the volume, which in our example is "/volume", and where it will be mounted in the container. Notice that when we pass the "--mount type=bind,src=/volume,dst=/volume" parameter, Docker mounted this directory in the container, but without any content.
We can also mount a volume in the container linking it with a directory from the host that already has some content. To illustrate, let's share the "/root/primeiro_container" directory, which we used to store our first dockerfile, and mount it in the container in a volume called "/volume" as follows:
# docker container run -ti --mount type=bind,src=/root/primeiro_container,dst=/volume ubuntu
root@3d372a410ea2:/# df -h
Filesystem Size Used Avail Use% Mounted on
none 13G 6.8G 5.3G 57% /
tmpfs 999M 0 999M 0% /dev
tmpfs 999M 0 999M 0% /sys/fs/cgroup
/dev/mapper/ubuntu--vg-root 13G 6.8G 5.3G 57% /volume
shm 64M 0 64M 0% /dev/shm
root@3d372a410ea2:/#
With this, we are mounting the "/root/primeiro_dockerfile" directory from the host inside the container with the name "/volume".
In the container:
root@3d372a410ea2:/# ls /volume/
Dockerfile
root@3d372a410ea2:/#
On the host:
root@linuxtips:~# ls /root/primeiro_dockerfile/
Dockerfile
root@linuxtips:~#
If I want to leave the volume in the container as read-only, it's possible. Just pass the "ro" parameter after the destination where the volume will be mounted:
# docker container run -ti --mount type=bind,src=/root/primeiro_container,dst=/volume,ro ubuntu
root@8d7863b1d9af:/# df -h
Filesystem Size Used Avail Use% Mounted on
none 13G 6.8G 5.3G 57% /
tmpfs 999M 0 999M 0% /dev
tmpfs 999M 0 999M 0% /sys/fs/cgroup
/dev/mapper/ubuntu--vg-root 13G 6.8G 5.3G 57% /volume
shm 64M 0 64M 0% /dev/shm
root@8d7863b1d9af:/# cd /volume/
root@8d7863b1d9af:/volume# ls
Dockerfile
root@8d7863b1d9af:/volume# mkdir teste
mkdir: cannot create directory 'teste': Read-only file system
root@8d7863b1d9af:/volume#
Just as it's possible to mount a directory as a volume, it's also possible to mount a file:
# docker container run -ti --mount type=bind,src=/root/primeiro_container/Dockerfile,dst=/Dockerfile ubuntu
root@df0e3e58280a:/# df -h
Filesystem Size Used Avail Use% Mounted on
none 13G 6.8G 5.3G 57% /
tmpfs 999M 0 999M 0% /dev
tmpfs 999M 0 999M 0% /sys/fs/cgroup
/dev/mapper/ubuntu--vg-root 13G 6.8G 5.3G 57% /Dockerfile
shm 64M 0 64M 0% /dev/shm
root@df0e3e58280a:/# cat Dockerfile
FROM debian
RUN /bin/echo "HELLO DOCKER"
root@df0e3e58280a:/#
This causes the "/root/primeiro_dockerfile/Dockerfile" file to be mounted at "/Dockerfile" in the container.
7.2. Creating Volumes​
Now let's create volumes the more elegant and modern way. Today we have the possibility to perform volume management in a very simple and intelligent manner.
Whenever we create a volume, it creates a directory with the same name inside "/var/lib/docker/volumes/".
In the following example, the "giropops" volume would then be created at "/var/lib/docker/volumes/giropops"; with this, all files available in this directory would also be available at the indicated location in the container. Let's see the examples! :D
It's possible to create volumes and perform all their administration through the command:
# docker volume create giropops
It's possible to remove it with the command:
# docker volume rm giropops
To check details about this volume:
# docker volume inspect giropops
To remove volumes that are not being used (use with extreme moderation! :D):
# docker volume prune
For you to mount the created volume in some container/service, simply execute the following command:
# docker container run -d --mount type=volume,source=giropops,destination=/var/opa nginx
Where:
-
--mount -- Command used to mount volumes.
-
type=volume -- Indicates that the type is "volume". There's also the "bind" type, where instead of indicating a volume, you would indicate a directory as the source.
-
source=giropops -- Which volume I intend to mount.
-
destination=/var/opa -- Where in the container I will mount this volume.
Simple as flying, right?
7.3. Locating Volumes​
If you want to get the location of your volume, it's simple. But for that you need to know the "docker volume inspect" command.
With "docker volume inspect" you can get details about your container, such as volume details.
The output of the "docker volume inspect" command returns more information than just the path of the directory on the host. Let's use the "--format" or "-f" option to filter the "inspect" output.
docker volume inspect --format {% raw %}'{{ .Mountpoint }}'{% endraw %} giropops
/var/lib/docker/volumes/giropopos/_data
7.3. Creating and Mounting a Data-Only Container​
A very interesting option regarding volumes relates to the data-only container, whose main function is to provide volumes for other containers. Remember the NFS server and Samba? Both centralized directories with the purpose of sharing among other servers. Well, the data-only container has the same purpose: to provide volumes to other containers.
One of Docker's great features is portability. A container created on your laptop should be portable enough to run in any other environment that uses Docker, in all corners of the universe!
That being so, what happens if I create an environment in Docker that tells containers to mount a directory from the local host? It depends. It depends on how this local host is. Questions like "does the directory exist?" "are the permissions adjusted?", among others, will define the success in executing the containers, which completely escapes Docker's scope.
Let's see how this works in practice! :)
For our example, first let's create a container called "dbdados", with a volume called "/data", which will store the data from a PostgreSQL database.
To create a container specifying a name for it, we use the "--name" parameter, as we'll see in the following example:
# docker container create -v /data --name dbdados centos
With this, we only created the container and specified a volume for it, but we haven't started it yet.
We know that in the container the volume is mounted at "/data". However, what is the location of this volume on the host?
Remember "docker inspect"? Let's use it again:
root@linuxtips:~# docker inspect -f {% raw %}{{.Mounts}}{% endraw %} dbdados
[{46255137fe3f6d5f593e9ba9aaaf570b2f8b5c870f587c2fb34f29b79f97c30c /var/lib/docker/volumes/46255137fe3f6d5f593e9ba9aaaf570b2f8b5c870f587c2fb34f29b79f97c30c/_data /data local true }]
Notice that now we used the container name instead of the "CONTAINER ID". Totally possible and much more intuitive.
When we executed "docker inspect", it returned the path of our volume. Let's see if there's any content inside it:
root@linuxtips:~# ls \
/var/lib/docker/volumes/46255137fe3f6d5f593e9ba9aaaf570b2f8b5c870f587c2fb34f29b79f97c30c/_data
As we saw, the directory doesn't have content yet.
Now let's create the containers that will run PostgreSQL using the "/data" volume from the "dbdados" container to store the data.
For us to do this example, we need to know two more very important parameters:
-
--volumes-from -- Used when we want to mount a volume provided by another container.
-
-e -- Used to inform environment variables to the container. In the example, we're passing PostgreSQL's environment variables.
Ready, now we're prepared! Let's create the containers with PostgreSQL:
# docker run -d -p 5432:5432 --name pgsql1 --volumes-from dbdados \
-e POSTGRESQL_USER=docker -e POSTGRESQL_PASS=docker \
-e POSTGRESQL_DB=docker kamui/postgresql
# docker run -d -p 5433:5432 --name pgsql2 --volumes-from dbdados \
-e POSTGRESQL_USER=docker -e POSTGRESQL_PASS=docker \
-e POSTGRESQL_DB=docker kamui/postgresql
To verify the two containers with PostgreSQL running, use "docker container ls".
Done, now we have the two containers with PostgreSQL running! Do we already have any data in the "/data" volume of the "dbdados" container?
Let's check again on the host if the volume now has any data:
root@linuxtips:~# ls /var/lib/docker/volumes/46255137fe3f6d5f593e9ba9aaaf570b2f8b5c870f587c2fb34f29b79f97c30c/_data
base pg_clog pg_ident.conf pg_notify pg_snapshots pg_stat_tmp pg_tblspc PG_VERSION postgresql.conf postmaster.pid server.key global pg_hba.conf pg_multixact pg_serial pg_stat pg_subtrans pg_twophase pg_xlog postmaster.opts server.crt
root@linuxtips:~#
Sensational! As we can see, the two PostgreSQL containers are writing their data to the "/data" volume of the "dbdados" container. It's tear-inducing! :D
7.4. A Backup Is Always Good​
Another really cool thing is the possibility of making backups of your data containers in a very simple and fast way.
Let's say you want to make a backup of the "/data" directory from the "dbdados" container that we created a while ago; how would we do it?
root@linuxtips:~# cd backup/
root@linuxtips:~/backup# docker run -ti --volumes-from dbdados -v $(pwd):/backup debian tar -cvf /backup/backup.tar /data
When we execute the previous command, a new container is created mounting the volume(s) from the "dbdados" container (which in this case is "/data", remember?). Additionally, the current directory from the host will be mounted in the "/backup" volume of the container, and then the tar command will be executed to package the "/data" directory inside the "/backup" directory. Pretty simple, right?
root@linuxtips:~/backup# ls
backup.tar
root@linuxtips:~/backup#
Remembering that volumes are always created inside "/var/lib/docker/volumes". If you want to make a backup of all volumes, just handle this directory in your backup routines. ;)