How volume mounting works in docker?

I have a very simple node js app and the project structure looks like this.

index.js

package.json

package-lock.json

Dockerfile

FROM node:12.18.2-alpine
WORKDIR /test-app
COPY package.json package-lock.json ./
RUN npm i
COPY . ./
EXPOSE 3000
ENTRYPOINT [ "node", "index.js" ]

docker-compose.yml

version: '3.2'

services:
  test-app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/test-app
      - "test_app_node_modules:/test-app/node_modules"
volumes:
  test_app_node_modules:
    driver: local

If you look at the volumes section in docker-compose.yml file, first I’m bind mounting my current directory on the host machine to the test-app directory on the container. This means :

  1. whatever files or directories that were inside my current dir will get reflected on the container dir and any changes made to the container dir will also get reflected back to the host dir.
  2. this means node_modules that were installed in the test-app dir of the container, during docker build, were overwritten as well.

and the next step in the volumes section is named volumes. This means:

  1. when the volume doesn’t exist, running for the first time, It should copy everything from test-app/node_modules inside the container to test_app_node_modules volume. But the test-app/node_modules is empty because step 1 overwrote it.
  2. which means we created an empty volume and mounted it to the container.

If this is so, it should be causing missing dependency error but my app is running properly. I’m not sure where I’m getting node_modules from.

Also, I see an empty node_modules folder in the host directory. I assume the reason behind this is "test_app_node_modules:/test-app/node_modules" looks for the node_modules in the container but it doesn’t exist so it creates one and as a result, it gets reflected back to the host dir.

I’m not able to grasp the idea of volume mounting. What is the flow here? How node_modules are begin stored into the volumes when there are none?

Did you take a look in /var/lib/docker/volumes/test_app_node_modules/_data and check wether any data is there?

Instead of reliying on the “copy on first use” mechanism of volumes, you might want to introduce an entrypoint script that a) checks wether a specific folder is empty and in case it is copy files from a temp folder to the target folder and b) start you node process. This will be more beginner friendly (who tend to use bind-mounts rather than volumes).

Do you think you can take a look at this example project?

I guess empty volumes will inherit files from the image instead of container. But i don’t see it anywhere in documentation.

Can you help a lazy guy and create the image and push it to dockerhub?

Did you check the folder’s content? If it’s populated than your observation might be validated. There is still a chance that the mount order is prone to a race condition, which played in your favor this time…

here is the docker image, but I’m using docke-compose for mounting volumes into container so you might have to clone the repo and build it yourself.

docker pull rawatnaresh/test-app:latest

yes, There’s data inside /var/lib/docker/volumes/test_app_node_modules/_data

I’m not sure no matter how many times you build it the results are always the same.

What do you think about this?

Here are the logs when i hit docker-compose up


naresh@naresh:~/Desktop/eg(master)$ docker-compose up
Creating network "eg_default" with the default driver
Creating volume "eg_test_app_node_modules" with local driver
Building test-app
Step 1/7 : FROM node:12.18.2-alpine
 ---> 057fa4cc38c2
Step 2/7 : WORKDIR /test-app
 ---> Running in dd9c24971f81
Removing intermediate container dd9c24971f81
 ---> 9fd168b96072
Step 3/7 : COPY package.json package-lock.json ./
 ---> faea6f3981d9
Step 4/7 : RUN npm i
 ---> Running in e62870b30b14
npm WARN test@1.0.0 No description
npm WARN test@1.0.0 No repository field.

added 50 packages from 37 contributors and audited 50 packages in 3.832s
found 0 vulnerabilities

Removing intermediate container e62870b30b14
 ---> 0e538ff07828
Step 5/7 : COPY . ./
 ---> f6038c6c15f5
Step 6/7 : EXPOSE 3000
 ---> Running in 12658de8319f
Removing intermediate container 12658de8319f
 ---> 025d402f5592
Step 7/7 : ENTRYPOINT [ "node", "index.js" ]
 ---> Running in dc9b9cde2c3f
Removing intermediate container dc9b9cde2c3f
 ---> f4341e6a4bbe
Successfully built f4341e6a4bbe
Successfully tagged eg_test-app:latest
WARNING: Image for service test-app was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating eg_test-app_1 ... done
Attaching to eg_test-app_1
test-app_1  | Server started at 3000

Yesterday I was not in the mood for a testdrive, though I tried it now.
I modified your docker-compose.yml snippet and replaced the build declaration with image and fired up the container.

Of course the container immediatly dies because I lack the applications in .:

internal/modules/cjs/loader.js:969
  throw err;
  ^

Error: Cannot find module '/test-app/index.js'
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:966:15)
    at Function.Module._load (internal/modules/cjs/loader.js:842:27)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
    at internal/main/run_main_module.js:17:47 {
  code: 'MODULE_NOT_FOUND',
  requireStack: []
}

But back to the main objective: you are right, the volume is populated with data. Your observations are correct. Can you open an issue on dockers Github pages regarding this missing detail in the documentation?

Though, may I suggest something? You might want to consider to mimic the volumes copy-on-create mechanism in an entrypoint script… Store the files in a temporary folder and copy the files to the target if the target folder is empty.

1 Like