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 the idea that bind mounts are synonymous with volumes. Maybe this is a holdover from my previous career, but, to quote Inigo Montoya:
For example, it's a common practice to have something like this as part of your app
service 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 ./
RUN npm install
COPY . .
COPY --chown=node:node . .
USER 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 acceptsmount
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.
Copyright © katjuell.site 2018