The Joy of Hex

Drunken Monkey Coding Style with a hint of Carlin humor

Jun 16, 2018 - 5 minute read - docker

Define named volume with host mount in the docker compose file

With the docker-compose version 3 and above out and about, one of the things that have been dropped is the volumes_from which was kind useful to share the volumes from one service with another. It was a nice feature for local development

So what can one do?

Well, now you can use bind mounts so your docker compose file will look along the lines of this:

docker-compose.yml
1
2
3
4
5
6
  web_app_cli:
    image: code4hire/dev-images:php-7.2-cli
    hostname: "web_app_cli"
    working_dir: /application
    volumes:
      - ./web-app/application:/application

Point of interest is line 6, which reads, form the host relative path ./web-app/application mount to container to /application. Seems kind of ok, but lets add some more services and see what happens.

docker-compose.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  web_app_cli:
    image: code4hire/dev-images:php-7.2-cli
    hostname: "web_app_cli"
    working_dir: /application
    volumes:
      - ./web/application:/application

  php-fpm:
    image: code4hire/dev-images:php-7.2-fpm
    volumes:
      - ./web/application:/application

  nginx:
      image: nginx:latest
      ports:
        - "8080:8080"
      volumes:
      - ./web/application:/application

So yeah, guess what happens if you need to either move your source or destination paths… What is the chance that you will miss a setting when the pressure is on? I’d say quite high.

This can be improved upon by using environment variable substitution

docker-compose.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  web_app_cli:
    image: code4hire/dev-images:php-7.2-cli
    hostname: "web_app_cli"
    working_dir: ${WEB_DESTINATION_PATH}
    volumes:
      - ${WEB_APP_PATH}:${WEB_DESTINATION_PATH}

  php-fpm:
    image: code4hire/dev-images:php-7.2-fpm
    volumes:
      - ${WEB_APP_PATH}:${WEB_DESTINATION_PATH}

  nginx:
    image: nginx:latest
    ports:
      - "8080:8080"
    volumes:
      - ${WEB_APP_PATH}:${WEB_DESTINATION_PATH}

and you create a .env file at the same level where your docker compose file is

.env
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# web specific mounts and settings

# Application's path (absolute or relative)
# **IMPORTANT**
# If you are using docker-sync, the path is relative to the location of this file
# if you are NOT using docker-sync, and just docker-compose,
# the path is relative to the location of docker-compose.yml
WEB_APP_PATH=./web/application

# Path inside of container where the application will be mounted,
# This var can also be used as workdir value for docker
# MAKE SURE THE VALUE IS IN SYNC WITH YOUR NGINX OR APACHE CONFIG
WEB_DESTINATION_PATH=/application

Now you have a little more flexibility, but it is still a lot of moving parts that might have to be changed at some point.

One important thing to note when dealing with .env and docker is the way Docker processes the keys and values, namely there is no special handling of quotation marks. This means that they are part of the VAL. You can read more about it here.

This might be common knowledge at this point, but I still wanted to write it down.

And this brings us to the actual subject of this post, I wanted to use named volumes to mount the host directory.

It started simple, and then it delved into weirdness.

For starters you need to define the named volume in your docker compose file, lets call the volume web_app

docker-compose.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  web_app_cli:
    image: code4hire/dev-images:php-7.2-cli
    hostname: "web_app_cli"
    working_dir: ${WEB_DESTINATION_PATH}
    volumes:
      - web_app:${WEB_DESTINATION_PATH}

  php-fpm:
    image: code4hire/dev-images:php-7.2-fpm
    volumes:
      - web_app:${WEB_DESTINATION_PATH}

  nginx:
    image: nginx:latest
    ports:
      - "8080:8080"
    volumes:
      - web_app:${WEB_DESTINATION_PATH}

volumes:
  web_app:
    driver: local
    driver_opts:
      type: bind
      device: ${WEB_APP_PATH}

So what is happening here is web_app volume is being declared, it should use a local driver, and now we come to the interesting part which is options, when reading volumes long syntax you would get the impression that the type needs to be set to bind, but once you execute docker composer you will get the following error

error
1
2
3
ERROR: for web_app  Cannot create container for service web_app: 
error while mounting volume with options: 
type='bind' device='./web/application' o='': no such device

So um, yeah, no. This is not going to work, after some digging around the net, the correct setup is

docker-compose.yml
1
2
3
4
5
6
7
8
9
...

volumes:
  web_app:
    driver: local
    driver_opts:
      type: none
      device: ${WEB_APP_PATH}
      o: bind

Which is completely intuitive… Riiiight… The type should be none, but the o should be set to bind. OK, that has been sorted now, and lets start it once again

Well, now… There is a different error, but this one actually is completely expected, as volumes, once created can’t be modified, just recreated

error
1
2
3
ERROR: Configuration for volume web_app specifies "o" driver_opt bind, but a volume with the same name uses a different "o" driver_opt (None). 
If you wish to use the new configuration, please remove the existing volume "web_app" first:
$ docker volume rm web_app

Before executing docker volume rm web_app make sure you execute docker-compose down so everything is stopped and released, otherwise you will get another error that the volume is in use

So now when you execute docker-compose up you are greeted with another error

error
1
2
3
ERROR: for web_app  Cannot create container for service web_app: 
error while mounting volume with options: type='bind' device='./web/application' o='bind': 
no such file or directory

Awesome! It turns out that the device setting only accepts the absolute paths. So what can be done? Either set the WEB_APP_PATH in your env vars (file or otherwise) to absolute path OR you use $PWD which most likely works only on linux and macos YMMV!

docker-compose.yml
1
2
3
4
5
6
7
8
9
...

volumes:
  web_app:
    driver: local
    driver_opts:
      type: none
      device: $PWD/${WEB_APP_PATH}
      o: bind

Don’t forget to remove the existing volume by executing docker volume rm web_app first. And now when you execute docker-compose up everything is working.

At this point you should have a named volume with host mounted directory that you can share amongst your services, because why not…

At the time of this writing I am not sure about the downsides of this approach, but as the syntax is very weird, I wanted to have it documented.

Of course this is ok for local development, and should not be used in production.