Persisting Storage in Docker

Generally speaking, Docker containers should have everything they need access to baked into the image. There are times, however, that it may be necessary to have additional files or directories provided to the container to persist information. These can include, but are not limited to:

  • Configuration files
  • Data persistence (usually only for local databases for development)
  • Application package hotswap during development
  • Saving artifacts generated by the application

Docker has two ways to provide such storage: bind mounts and volumes.

Bind Mounts

Bind mounts are used to provide access to a directory on the host machine. On a Linux host, Docker allows you to bind a user defined directory into the root filesystem of the container, effectively allowing you to do the equivalent of mount –bind for your directly to link it directly to the container’s filesystem. This is ideal for providing custom configuration files or saving off build artifacts to your host directory. To mount a directory into a container, execute the following on an example container:

mkdir -p testsite
echo "Hello, world!" > testsite/index.html
docker run -d --rm --name test --mount type=bind,source=$(pwd)/testsite,target=/usr/share/nginx/html -p 80:80 nginx

This creates a simple dummy site and then pulls down the nginx image, running it with the content of our simple site. If you open your web browser to http://localhost, you will see the “Hello, world!” message that we left in our sample directory. Alternatively, instead of the –mount option, you can use the older style -v syntax:

docker run -d --rm --name test -v $(pwd)/testsite:/usr/share/nginx/html -p 80:80 nginx

It is recommended that you use the –mount  option as it is more precise in its definition. The -v  option is only still available for legacy purposes.

We can inspect the container and see that the mount is defined for our container by using docker inspect test :

...
        "Mounts": [
            {
                "Type": "bind",
                "Source": "/tmp/testsite",
                "Destination": "/usr/share/nginx/html",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            }
        ],
...

We can specify bind mounts in a compose file as such:

version: "3.2"
services:
  web:
    image: nginx:alpine
    volumes:
      - type: bind
        source: ./testsite
        target: /usr/share/nginx/html

You can also map an individual file onto a container, but it is rare to do so. If using the -v syntax, note that if the file is missing, a directory will be created with the name of the file that you specify. This can be confounding if you use this in a Compose file. More can be found on the Docker website:

https://docs.docker.com/storage/bind-mounts/

Bind mounts on Docker for Mac do not use native bind mounting, but instead uses osxfs to attempt to provide a near-native experience for bind mounts. It is still slower than a native bind mount running on Linux, but should still work seamlessly with local HFS+ filesystems. By default, it only has access to the /Users, /Volumes, /private, and /tmp directories. See official documentation details on Docker’s official website:

https://docs.docker.com/docker-for-mac/osxfs/

Docker Volumes

Docker volumes are file system mounts that are managed completely by the Docker engine. Historically, these have been called “named volumes,” just in case you see a reference to it in literature or in command line help or error messages. When a Docker volume is created, the directory is stored in the /var/lib/docker/volumes/  directory. The typical use case for a named volume would be for something like data persistence or sharing data between containers. Let’s dig out the Pastr app from the first tutorial. We’ll add the mount in the docker-compose.yml  file:

  database:
    image: redis:latest
    volumes:
      - type: volume
        source: pastrdatastore
        target: /data
    ports:
      - "6379:6379"
...
volumes:
  pastrdatastore:

The top-level volumes directive (at the bottom of the snippet) denotes that a datastore shall be created via this compose file. After starting the container with docker-compose up -d the Docker engine will create the pastrdatastore volume.

$ docker volume ls
DRIVER              VOLUME NAME
local               pastr_pastrdatastore
$ docker volume inspect pastr_pastrdatastore 
[
    {
        "CreatedAt": "2018-09-25T20:13:41-05:00",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "pastr",
            "com.docker.compose.version": "1.22.0",
            "com.docker.compose.volume": "pastrdatastore"
        },
        "Mountpoint": "/var/lib/docker/volumes/pastr_pastrdatastore/_data",
        "Name": "pastr_pastrdatastore",
        "Options": null,
        "Scope": "local"
    }
]

It creates it a volume in the  /var/lib/docker/volumes  directory. This volume is mounted to the database container, which we see when we inspect it with docker inspect pastr_database_1 .

        "Mounts": [
            {
                "Type": "volume",
                "Name": "pastr_pastrdatastore",
                "Source": "/var/lib/docker/volumes/pastr_pastrdatastore/_data",
                "Destination": "/data",
                "Driver": "local",
                "Mode": "rw",
                "RW": true,
                "Propagation": ""
            }
        ],

Note that on a Linux machine, this volume exists on the native filesystem. However, on a Windows or Mac system, this volume exists within the virtual machine; you can’t access it directly, nor should you try to, even on a Linux machine. If you need to mount the data store to inspect its contents, you can run it with docker run -it –rm –mount source=pastr_pastrdatastore,destination=/mnt ubuntu /bin/bash.

For more details, please see the official Docker documentation:

https://docs.docker.com/storage/volumes/#start-a-service-with-volumes

Leave a Reply

Your email address will not be published. Required fields are marked *