Docker: Managing data with Volumes (Part 2) | Manvi Sharma

Post

Docker: Managing data with Volumes (Part 2)

Hello everyone,

In the last post, we discussed the concept of volumes. We discussed the use of named and anonymous volumes.

In this post, we will discuss the need and use of bind mounts

We will learn the use of .dockerignore files. We will also see working with environment variables and .env files. We will also learn the use of build arguments.

Need for bind mounts:

So far using anonymous and named volumes, we have been able to persist temporary app data. However, during the development of our application, new changes made in our source code are not reflected in our app until we rebuild our image. During development, we change a lot of code and hence cannot build again and again.

This is where build mounts are needed and help us. Bind mounds save data in a location on the host file system, not tied to any specific container. Data can be shared across containers.

We could put our source code into a bind mount and make sure our container is aware of that. The source code then is not used from the path in COPY . . command but from bind mount.

Making Bind Mounts

Bind mounts are perfect for persistent and editable data. Data survives container shutdown/restart – removal on the host and can be reused for the same container (across restarts).

To use bind mount follow the following steps:

We already have our image ready, we just need to use it to run the new container. Like named volumes, bind mounts are also added while running the container.

docker run -p 3001:80 -d --rm --name feedback-app -v feedback:/app/feedback -v "/Users/manvisharma/Projects/Docker/Part 2/data-volumes-01-starting-setup":/app feedback-node:volumes

As seen in the command above, we have kept the named volumes since that will persist our application data(i.e., our feedbacks), but for bind mounts, we have added another volume using -v flag, and the first value before the colon : , is the absolute path of our application in our system.

The second value is our application inside the container. The absolute path will be different for all of you depending on where you have saved your project. I have mentioned mine in the command.

Note: We also have alternates for not writing the full path while creating bind mounts.
MacOs : -v $(pwd):/appWindows: -v "%cd%":/app

Note: Docker should have access to the folder whose absolute path has been shared. We can set this in preferences/settings< Resources<File Sharing section.
media

Also, after running the command mentioned above, the container crashes and deletes automatically due to the --rm flag.

To check the error, just run the container without --rm flag, and check for docker logs feedback-app.

media

The error above occurs because of the bind mount we created. So the new volume we created seems to override everything, even node_modules. Since the node_modules are not present in the local folder. This is logical since the local node_modules can be not updated, or maybe of some old version. This can create inconsistency here.

So the fix here is to add an anonymous volume for node_modules. The rule of docker is that it prefers the longest clear internal path. So, it will first save node_modules in the container, and then override the container code with the local code and this is the use of anonymous volumes in Docker along with bind mounts.

docker run -p 3001:80 -d --rm --name feedback-app -v feedback:/app/feedback -v "/Users/manvisharma/Projects/Docker/Part 2/data-volumes-01-starting-setup":/app -v /app/node_modules feedback-node:volumesORdocker run -p 3001:80 -d --rm --name feedback-app -v feedback:/app/feedback -v $(pwd):/app -v /app/node_modules feedback-node:volumes

These changes work.

Now, if we make some changes in our HTML files. They are reflected side by side on our application, without having to rebuild the Docker image. This is how build mounts make development easier.

Using bind Mounts with Nodejs:

While working with bind mounts above, we made our HTML development easier. But what if I make changes to the server.js file? That is nodejs code and is executed during runtime. If you have worked with nodejs, you might realise, that we need to restart the node server again after making changes, or we can use nodemon if we do not wish to do it. We will do the same here so that we can work with nodejs and docker together.

As of now if we make any changes in the server.js file. E.g. Add a console.log statement. Then check for docker logs, I cannot find any logs here. Again code is updated in our container because of the bind mount we created, but the node server is not updated. I will fix this by adding nodemon package. We do not have to install anything, rather just update package.json file and CMD options in Docker images. Then rebuild the Docker image and run the container with volumes.

package.jsondependencies

media

Dockerfile

FROM nodeWORKDIR /appCOPY package.json .RUN npm installCOPY . .EXPOSE 80CMD [ "npm", "start" ]

On integrating nodemon, the server changes are also reflected:

media
Note: The above-mentioned method is not only true for node, but for any technology, we would have our application in to be integrated with docker.
Note: Though obvious I would like to highlight that, if we want to run an app with fresh data at this point, we will have to delete our volumes saved locally on our machine.

Optimization using Anonymous Volumes:

If we check our code in the server.js file, first it copies our feedback to temp folder and check if the feedback already exists. If it exists, it does not save feedback, and if it does not exist, it saves our feedback to the feedback folder.

This computation now happens inside our container. If we create an anonymous volume for our temp folder, all computation will be handled locally rather than inside the docker container. This is how we can use anonymous volumes for optimization.

docker run -p 3001:80 -d --rm --name feedback-app -v feedback:/app/feedback -v $(pwd):/app -v /app/node_modules -v /app/temp feedback-node:volumes

Read-Only Volumes

By default, all volumes are read-write. The bind that we have created should not be updated by the container. So this volume can be made read-only. We can add an ro(read-only) option, with the bind mount to do this.

docker run -p 3001:80 -d --rm --name feedback-app -v feedback:/app/feedback -v $(pwd):/app:ro -v /app/node_modules -v /app/temp feedback-node:volumes

Create new volume

docker volume create VOLUME_NAME

COPY vs BIND MOUNTS

Since our bind mounts make the entire application code available to our container, we might not need COPY . .command in our Dockerfile. If I try to do this, it works, we do not get any issues. But we should completely avoid such practices. This is because, during deployment, we will not create our container using bind mounts. You will realise this when we will explore deployment. We might store things somewhere else.

We will prefer to have a snapshot of our code.

.dockerignore File

We can add "to-be-ignored" files and folders in our this file. This works just like a .gitignore file. In general, we want to add anything that isn't required by our application to execute correctly.

For example, Add the following entries:

1. Dockerfile

2. .git

3. node_modules

This would ignore the Dockerfile as well as a potentially existing .git folder (if we are using Git in our project).

We do not need to copy everything as specified by COPY. .. We can make a .dockerignore file and add node_moduleshere, so that if there are any node_modules locally they do not override the ones present in the container, as the ones present locally can be outdated.

Arguments and Environment variables

Docker supports build-time ARGuments and runtime ENVironment variables.

ENV:

-Available inside of Dockerfile & in application code

-Set via ENV in Dockerfile or via --env on docker run

If you have worked in Node, you might know that Node embraces the concept of environment variables.

We add these variables in Dockerfile.

FROM nodeWORKDIR /appCOPY package.json .RUN npm installCOPY . .ENV PORT 80EXPOSE $PORTCMD [ "npm", "start" ]

We can also customise it further using the docker run command while running container

docker run -p 3001:8000 --env PORT=8000 -d --rm --name feedback-app feedback-node

So PORT 80 in Dockerfile is just a default value. Changing this with docker run updates our value. We have to update port with -p flag accordingly.

We can also use -e flag instead of --env flag. We can specify multiple variables like

-e PORT=8000 -e.... so on

We can also use a dedicated env file.

docker run -p 3001:80 --env-file ./.env -d --rm --name feedback-app feedback-node

ARG

-Available inside of Dockerfile

-NOT accessible in CMD or any application code

-Set on image build(docker build) via --build-arg

We can also add build-time arguments in Dockerfile.

FROM nodeWORKDIR /appARG DEFAULT_PORT=80COPY package.json .RUN npm installCOPY . .ENV PORT $DEFAULT_PORTEXPOSE $PORTCMD [ "npm", "start" ]

We can now customise this while building the Docker image.

docker build -t feedback-node:dev --build -arg DEFAULT_PORT=8000

So again 80 is the default value, but we can give our own custom value.

That's all about Bind mounts and other related concepts. We will continue in the next post.