1. Overview
Docker volumes are used for persistent data. These data should exist even after the container is removed or recreated. An example of this is database data files. We usually define volumes in the Dockerfile as part of the image or when we create a container with the Docker command line interface.
This tutorial will examine ways to add a volume to an existing container. This can be useful in cases where we want to keep the current state of a container. However, best practices dictate the ability to recreate a container from its image at any time.
2. Sample Container Creation
Let’s start by creating a sample container from Alpine Linux:
$ sudo docker container create --name my-alpine -it alpine:latest /bin/sh
Here, we’ve named our container my-alpine. Next, we start the container in interactive mode via -i:
$ sudo docker container start -i my-alpine
#
Then, in the container, we create the /opt/baeldung directory and exit:
/ # mkdir /opt/baeldung
/ # exit
After exiting, the container stops running. As a result, we have a container with /opt/baeldung already created, but no volumes.
Now, let’s create the volume that we’ll add to our container:
$ sudo docker volume create my-volume
Here, we’ve named our volume my-volume.
3. The Export and Import Commands
We can export the filesystem of a Docker container with the export command. The result is a TAR archive.
In fact, we can import this archive as an image. Next, we can recreate our container from the exported image without losing any changes, such as our /opt/baeldung directory. Finally, we can add our volume to the recreated container.
Let’s start with the export:
$ sudo docker container export -o myalpine.tar my-alpine
As expected, we’ve saved the container’s filesystem on a file with the name myalpine.tar. Let’s extract the file with tar to check its contents:
$ sudo tar xvf ../myalpine.tar
$ ls -al
total 76
...
-rwxr-xr-x 1 root root 0 Sep 28 15:35 .dockerenv
drwxr-xr-x 2 root root 4096 Aug 9 11:47 bin
drwxr-xr-x 4 root root 4096 Sep 28 15:35 dev
drwxr-xr-x 16 root root 4096 Sep 28 15:35 etc
drwxr-xr-x 2 root root 4096 Aug 9 11:47 home
...
Indeed, we see that we’ve exported the entire filesystem of the my-alpine container. Moreover, we can find the baeldung subdirectory that we created in the /opt directory. Next, let’s import the tar archive with the import command:
$ sudo docker image import myalpine.tar my-alpine-restored
$ sha256:...
$ $ sudo docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
my-alpine-restored latest b78e5fa65b50 7 seconds ago 5.54MB
my-alpine latest bc705bf3d56b 24 hours ago 5.54MB
As we can see, we’ve created a new image from the TAR file with the repository name my-alpine-restored. This new image will contain the /opt/baeldung directory we’ve created. Finally, let’s create a new container and mount our volume to it:
$ sudo docker container create --name my-restored-alpine --mount source=my-volume,target=/opt/my-volume -it my-alpine-restored /bin
/sh
/ # ls /opt
baeldung my-volume
As a result, we managed to clone our container and add a volume to it.
4. The Commit Command
Instead of the export and import commands, we can use the commit Docker command. The commit command creates a new image from an existing container.
Similarly to the previous section, our goal is to clone the container along with its state. Thus, we’ll create a new image from the container with the commit command. Then, we’ll create a clone container from the new image and attach our volume to it.
This time, we’ll start with the commit:
$ sudo docker container commit my-alpine my-alpine-committed
$ sha256:...
With the command above, we’ve created a new image named my-alpine-committed using the my-alpine container. In fact, we can verify this by listing the available images:
$ sudo docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
my-alpine-committed latest b2e0deec1dd9 23 minutes ago 5.54MB
my-alpine latest abcc1c0330ec 7 days ago 5.54MB
As expected, we can see the my-alpine-committed image listed. Next, let’s create a new container and mount our volume to it:
$ sudo docker container create --name my-alpine-committed -it --mount source=my-volume,target=/o
pt/my-volume my-alpine-committed /bin/sh
Here, we created a new container with the name my-alpine-committed, which is a clone of the my-alpine container with a mounted volume. As before, we mount the volume inside the /opt directory. We can verify this by starting the container:
$ sudo docker container start -i my-alpine-committed
/ # ls /opt
baeldung my-volume
Indeed, both the volume and the baeldung subdirectory exist in the /opt directory. So, we’ve successfully cloned our container with its state and added a volume to it.
5. Adding a Volume by Modifying the config.v2.json File
Docker stores container settings in special configuration files within its directory structure. On Ubuntu, we can find these files in the /var/lib/docker directory. Each container has a corresponding subdirectory in the /var/lib/docker directory, named after the container ID.
Notably, we’re not supposed to modify these files manually. One reason for this is that our changes wouldn’t be permanent until we restart the Docker daemon.
In other words, until we perform a restart of the Docker daemon, any Docker command we execute may override our changes. So, we may prefer one of the previous solutions to the problem.
Firstly, we have to find out the full container ID. We can do this with the inspect command. In addition, we can filter its output with the grep command to keep only the ID:
$ sudo docker container inspect my-alpine | grep "Id"
"Id": "4a5f380f794de953b97d4a8f4f7f5a1dd277091f090b1257c9886e0302a2acd5",
Next, let’s search for the config.v2.json file of our container:
$ sudo ls /var/lib/docker/containers/4a5f380f794de953b97d4a8f4f7f5a1dd277091f090b1257c9886e0302a2acd5/config.v2.json
/var/lib/docker/containers/4a5f380f794de953b97d4a8f4f7f5a1dd277091f090b1257c9886e0302a2acd5/config.v2.json
So, we’ve found one of the main configuration files for our container. The config.v2.json contains the settings for mounting a volume. Now, we can edit this file and add the volume settings. Note that the JSON content is not formatted, so we may use a formatting tool for our ease. We should find the “MountPoints” element and replace it:
"MountPoints": {
"/opt/my-volume": {
"Source": "/var/lib/docker/volumes/my-volume/_data",
"Destination": "/opt/my-volume",
"RW": true,
"Name": "my-volume",
"Driver": "local",
"Type": "volume",
"Relabel": "z",
"Spec": {
"Type": "volume",
"Source": "my-volume",
"Target": "/opt/my-volume"
},
"SkipMountpointCreation": false
}
}
In the above snippet, we set our container to mount the my-volume volume in the /opt folder. Let’s load our changes by restarting the Docker daemon:
$ sudo systemctl restart docker
Here, we restart the Docker daemon with the systemd service manager. We can inspect our container to verify that Docker has loaded our changes:
$ sudo docker container inspect my-alpine
Finally, let’s start our container again to confirm that the volume is mounted:
$ sudo docker container start -i my-alpine
/ # ls /opt
baeldung my-volume
Once again, we managed to load the volume. This time, we did it without cloning our container.
6. Conclusion
This article looked at three solutions for adding a volume to an existing container. The first uses the export and import commands, while the second employs the commit command. In both of them, we created a clone container and added a volume to it. In the third solution, we edited the configuration file of the container to add the volume.