Skip to content

Instantly share code, notes, and snippets.

@creachadair
Last active March 8, 2019 15:26
Show Gist options
  • Save creachadair/c0a338d5e384d823c7a4f4938612e5ed to your computer and use it in GitHub Desktop.
Save creachadair/c0a338d5e384d823c7a4f4938612e5ed to your computer and use it in GitHub Desktop.
Notes on volumes and mountpoints in Docker

Notes on Volumes and Mountpoints

The following are some lessons I've learned from stumbling around with volumes and mountpoints in Docker. At the outset let me be clear that there is no new information here -- everything described below can be found in the user documentation in one form or another. However, I spent a bunch of time screwing things up for lack of context, so I am recording some of my lessons here so I can find them again.

The Old and the New

There are broadly two ways external filesystems can be attached to a container:

  1. As a pass-through to an existing filesystem on the host. This is what Docker calls a "bind mount", and it basically corresponds with the similarly-named Linux and Plan9 notion.

  2. As a managed filesystem provided by the container execution environment. This is what Docker calls a "volume". Unlike a bind mount, a volume is entirely mediated by Docker.

The documentation strongly implies that volumes (2) are preferable to bind mounts (1). However the two features are not strictly comparable, and both have their place. A more useful interpretation of this advice is that volumes are a better fit for cases where you're attaching a filesystem to a container for persistent storage. Since that is probably the predominant use of external filesystems, the advice makes sense in that context.

UI Confusion

The UI for attaching external filesystems to Docker containers has a pointlessly confusing legacy. Specifically, there are two distinct flags you can use: --volume (or -v), and --mount. I assumed at first that --volume was for volumes and --mount was for bind mounts, but in fact it's mainly a syntactic difference arising from some uninteresting historical nonsense.

For many purposes you can use --volume and --mount interchangeably, modulo their argument syntax:

--volume <host-fs-or-volume>:<mount-path>[:<csv-options>]
--mount  type=<type>,source=<src>,target=<tgt>,readonly,volume-opt=<csv-kv-opts>

Each can be used to specify both bind-mounts or volume-mounts. There are various pointless restrictions on which flag works in certain circumstances, but broadly speaking I use the rule of thumb that if you only care about source and target --volume is fine; otherwise use --mount.

Compile Time vs. Runtime

Insofar as a compiler is basically an interpreted DSL for code generation, docker build is a compiler whose input is a jumped-up shell script (née Dockerfile) and whose output is an ordered bundle of overlays that record everything the script touched. During the compilation the script can do anything it wants (more or less), including copy files from the host, fetch things over the network, and run arbitrary processes in the containers the compiler provides. The compiler runs it each step in a sandbox and records any changes it made to the filesystem.

The compiler does not know the contents of external volumes that will be mounted after the image is finished. The input can stipulate (via the VOLUME <path> instruction) that a certain place in the filesystem will have something attached to it (either from the host or from another container), but not what. This is intentional, since a given image may be attached to many different containers, each with different volumes mounted.

During compilation, whatever already exists under the <path> touched by a VOLUME <path> instruction remains visible to the instructions after that point. However, any changes made through that path are discarded. My understanding of it so far is that mountpoint paths are transparent-but-read-only at compile time, and opaque-but-possibly-writable at runtime (masked by whatever got attached, and "possibly" depending on whether readonly was set).

The upshot of this distinction is that the Dockerfile cannot do things with (or to) the contents of external volumes.

Managed Volume Setup

Creating a managed volume is easy enough:

docker volume create foo

Populating it is a little more tedious. The recommended way is using docker cp, but to do that you need a container. Fortunately it doesn't have to be a useful container, or even a running container:

docker run --name foo-init --mount=src=foo,dst=/foo busybox true

Even though this container won't even be running by the time you check, you can copy to it:

docker cp data foo-init:/foo

And then clean up the stub container:

docker rm foo-init

Similarly, if you want to see what's in the volume, you'll have to create a container with it mounted. As long as you mount the volume writably, you can use this to make manual edits, e.g.,

 docker run -it --rm --mount=src=foo,dst=/foo busybox sh

Nesting

It works to nest mountpoints. The documentation doesn't actually spell out the semantics of this, but I kind of assume it's intended to be this way. You can say, for example:

docker run --mount=src=foo,dst=/alpha --mount=src=bar,dst=/alpha/tango ...
#                              ^^^^^^                     ^^^^^^^^^^^^
# note that the latter shares a prefix with the former.

and the resulting filesystem will do what you might expect, viz.,

/alpha       #  from volume foo
   beta      #  "
   gamma     #  "
   tango/    #  from volume bar
     uniform #  "
   zeta      #  from volume foo

As far as I can tell, if foo has stuff under tango/... already, it will be masked by the subordinate mount. That would make sense, but isn't documented anywhere I could find.

Inspecting Volumes

Using docker volume inspect <volume-name> will give a JSON blob that describes the volume, including where its data are stored in the filesystem, e.g.,

$ docker volume inspect bblfsh-storage
[
    {
        "CreatedAt": "2019-02-06T18:39:15Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/bblfsh-storage/_data",
        "Name": "bblfsh-storage",
        "Options": null,
        "Scope": "local"
    }
]

The "Mountpoint" key indicates where the data are stored. On macOS, however, that path is inside the OS VM, not the host filesystem, so you cannot simply visit that path in the Finder to browse the files. Instead, one simple strategy is to run a container with the root bind-mounted someplace, e.g.,

$ docker run --rm -it -v /var/lib/docker/volumes:/volumes alpine:latest /bin/sh

By binding /var/lib/docker/volumes (in the VM) to /volumes (in the container), you can resolve volume mountpoints like /var/lib/docker/volumes/foo as /volumes/foo inside the container you just started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment