Some Thoughts on Docker Volumes

Quick tips for working with Docker volumes

Posted by katjuell on November 29, 2018

This post is intended to collect some thoughts about Docker volumes. It's going to cover topics that I've encountered while writing and editing, including: challenges when working with bind mounts in development, issues to think about with permissions, and cool things you can do with named volumes as you get your setup ready for deployment. This post is not meant to be comprehensive; instead, it's meant to be a focused set of tips for getting the most out of your volumes.

Bind Mounts

Probably the most common ambiguity I've encountered while working with Docker volumes is in the use of the term itself. According to the Docker docs, you have two choices when it comes to mounting your data: volumes and bind mounts. So when we use the term volume, we are referring to this type of mount. Maybe this is a holdover from my previous career, but when I see the term 'volume' in some contexts, I have a bit of an Inigo Montoya moment:

Inigo Montoya quote

This is especially important when thinking about people who may be new to Docker. For example, it's a common practice to have something like this as part of an app dev service setup in a docker-compose.yml file:


                  #App Service
                    app:
                    ...
                      volumes:
                        - ./:/app
                    ...
                  

This, my friends, is a bind mount, which mounts the contents of a specified directory on the host to a specified location on the Docker container. For development setups, bind mounts can prove handy, since they allow you to make synchronous changes to your code on the container and host. If your project is in active development this can be quite useful.

However....

There are also pitfalls. For example, let's say you have a Dockerfile for a Node.js application that looks like this:


                  FROM node:10

                  WORKDIR /app
                  
                  COPY package*.json ./
                  
                  RUN npm install
                  
                  COPY . .
                  
                  EXPOSE 8080

                  CMD [ "node", "app.js"]
                  

In addition, you have an app service defined in your docker-compose.yml file that looks like this:


                  #App Service
                    app:
                    ...
                      volumes:
                        - ./:/app
                    ...
                  

Well, the care that you extended in creating your node_modules directory from scratch on the container will be for naught, since whatever is included in your node_modules directory on the host will now copy to your container. Thanks for that, bind mount!

There are ways around this, of course, but it adds a layer of complexity that you should be aware of if you plan to use bind mounts in development. Any bind mount you define in your docker-compose.yml file (or files) will override what you do in your Dockerfile.

Permissions

Docker's default behavior is to mount volumes as root. It is worth keeping this behavior in mind as you think about things like running containers as a non-root user. For example, the official Node Docker image maintains that it is a good idea to run the container as a non-root user. This can pose challenges when working with volumes. It is important, for example, to set permissions in your Dockerfile that establish ownership any time there's an installation or copy. So, for example, the Dockerfile we looked at above might become something like the following if you want to use the Node image's available node user:


                  FROM node:10

                  RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app

                  WORKDIR /home/node/app

                  COPY package*.json ./

		  USER node

                  RUN npm install

                  COPY --chown=node:node . .

                  EXPOSE 8080

                  CMD [ "node", "app.js" ]
                  

Another potential pitfall when working with bind mounts is that Docker will create the directory you specify in the docker-compose.yml file on the host if it doesn't already exist — as (you guessed it) root. If, for example, you wanted to run an installation process on a container and then mount the code on your host, you could end up with a messy situation if you are working as a non-root user. Consider using containers to mount your application code to the host instead, if you are looking to take advantage of a containerized workflow. The first step of this article, for example, describes how to use Docker's composer image to mount the necessary dependencies for a Laravel project to the host.

Named Volumes

If you are working on a deployment setup, then you will want to think about using named volumes instead of bind mounts. According to Docker, named volumes are the preferred mechanism you should use when sharing data between containers. Things to keep in mind:

  • Volumes are empty on start — as will be any folders they are mounted to on the container.
  • The contents of a volume are the result of service actions at runtime.
  • The default driver for a created volume is the local driver.
  • The local driver on Linux systems accepts mount command options.

For more on points two and three above, see Docker's documentation of the docker volume create command.

This means that if you are looking for an efficient way to share data between containers in a deployment setup, named volumes are the way to go. You can use mount options to populate them with code from your host. For example, if you had a project directory with your application code, and you wanted to share that code between two containers, you could create a named volume and use it in both service definitions:


                  #App Service
                    app:
                    ...
                      volumes:
                        app-code:/app

                    webserver:
                    ...
                      volumes:
                        app-code:/var/www/html
                    ...

                  volumes:
                    app-code:
                      driver: local
                      driver_opts:
                        type: none
                        device: /home/your_user/your_project/
                  ...
                  

Your volume, app-code, would be populated at runtime with the contents of the project directory (here your_project), which would then copy over to the containers. This prevents some of the ambiguity of using bind mounts, though you would still need to think about permissions, since these volumes would be mounted as root.

Conclusion

This post is an attempt to summarize some of the most relevant things I've run into when working with Docker volumes and running containers as a non-root user. Docker volumes are an extensive topic, and I hope to have an opportunity to return to them in the future.