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.