I never recommend changing files in the docker data root, but I tried your way with Docker CE 27.1.2 and worked perfectly. I don’t think it matters, but my backing filesystem was ext4 and not xfs as yours. I also had an officially supported operating system, Ubuntu, not Rocky Linux.
Since it works for me with local named volumes, that could be a bug but your post made me curious and I played with the settings and the API a little.
I enabled the debug mode in the docker daemon:
{
"log-level": "debug"
}
systemctl restart docker
Then I tried
services:
web:
image: nginx
volumes:
- type: volume
source: data_bind
target: /app
volume:
nocopy: true # I tried without this first
subpath: tftp
volumes:
data_bind:
driver: local
driver_opts:
type: none
device: ./data
o: bind
I got the error message:
Error response from daemon: invalid mode: rw,nocopy,tftp
So I checked the API error log
journalctl -e -u docker | grep 'form data'
My last entry was the json of the container create api call
Aug 30 00:29:20 docker-vm dockerd[6732]: time="2024-08-30T00:29:20.854456037+02:00" level=debug msg="form data: {\"AttachStderr\":true,\"AttachStdin\":false,\"AttachStdout\":true,\"Cmd\":null,\"Domainname\":\"\",\"Entrypoint\":null,\"Env\":null,\"HostConfig\":{\"AutoRemove\":false,\"Binds\":[\"test_data_bind:/app:rw,nocopy,tftp\"],\"BlkioDeviceReadBps\":null,\"BlkioDeviceReadIOps\":null,\"BlkioDeviceWriteBps\":null,\"BlkioDeviceWriteIOps\":null,\"BlkioWeight\":0,\"BlkioWeightDevice\":null,\"CapAdd\":null,\"CapDrop\":null,\"Cgroup\":\"\",\"CgroupParent\":\"\",\"CgroupnsMode\":\"\",\"ConsoleSize\":[0,0],\"ContainerIDFile\":\"\",\"CpuCount\":0,\"CpuPercent\":0,\"CpuPeriod\":0,\"CpuQuota\":0,\"CpuRealtimePeriod\":0,\"CpuRealtimeRuntime\":0,\"CpuShares\":0,\"CpusetCpus\":\"\",\"CpusetMems\":\"\",\"DeviceCgroupRules\":null,\"DeviceRequests\":null,\"Devices\":null,\"Dns\":null,\"DnsOptions\":null,\"DnsSearch\":null,\"ExtraHosts\":[],\"GroupAdd\":null,\"IOMaximumBandwidth\":0,\"IOMaximumIOps\":0,\"IpcMode\":\"\",\"Isolation\":\"\",\"Links\":null,\"LogConfig\":{\"Config\":null,\"Type\":\"\"},\"MaskedPaths\":null,\"Memory\":0,\"MemoryReservation\":0,\"MemorySwap\":0,\"MemorySwappiness\":null,\"NanoCpus\":0,\"NetworkMode\":\"test_default\",\"OomKillDisable\":false,\"OomScoreAdj\":0,\"PidMode\":\"\",\"PidsLimit\":null,\"PortBindings\":{},\"Privileged\":false,\"PublishAllPorts\":false,\"ReadonlyPaths\":null,\"ReadonlyRootfs\":false,\"RestartPolicy\":{\"MaximumRetryCount\":0,\"Name\":\"\"},\"SecurityOpt\":null,\"ShmSize\":0,\"UTSMode\":\"\",\"Ulimits\":null,\"UsernsMode\":\"\",\"VolumeDriver\":\"\",\"VolumesFrom\":null},\"Hostname\":\"\",\"Image\":\"nginx\",\"Labels\":{\"com.docker.compose.config-hash\":\"ca204030ebfa4e6e176ee2e383c1f0f9695fb76240141c92fd8d307909068d82\",\"com.docker.compose.container-number\":\"1\",\"com.docker.compose.depends_on\":\"\",\"com.docker.compose.image\":\"sha256:a9dfdba8b719078c5705fdecd6f8315765cc79e473111aa9451551ddc340b2bc\",\"com.docker.compose.oneoff\":\"False\",\"com.docker.compose.project\":\"test\",\"com.docker.compose.project.config_files\":\"/home/ubuntu/test/compose.yml\",\"com.docker.compose.project.working_dir\":\"/home/ubuntu/test\",\"com.docker.compose.service\":\"web\",\"com.docker.compose.version\":\"2.29.1\"},\"NetworkingConfig\":{\"EndpointsConfig\":{\"test_default\":{\"Aliases\":[\"test-web-1\",\"web\"],\"DNSNames\":null,\"DriverOpts\":null,\"EndpointID\":\"\",\"Gateway\":\"\",\"GlobalIPv6Address\":\"\",\"GlobalIPv6PrefixLen\":0,\"IPAMConfig\":null,\"IPAddress\":\"\",\"IPPrefixLen\":0,\"IPv6Gateway\":\"\",\"Links\":null,\"MacAddress\":\"\",\"NetworkID\":\"\"}}},\"OnBuild\":null,\"OpenStdin\":false,\"StdinOnce\":false,\"Tty\":false,\"User\":\"\",\"Volumes\":null,\"WorkingDir\":\"\"}"
But it shows other things before the log. so I made this command to make it compatible with jq and show only valid json outputs
journalctl -e -u docker \
| grep 'form data' \
| sed 's/.*form data: /"/' \
| jq 'fromjson'
This was my last json output:
{
"AttachStderr": true,
"AttachStdin": false,
"AttachStdout": true,
"Cmd": null,
"Domainname": "",
"Entrypoint": null,
"Env": null,
"HostConfig": {
"AutoRemove": false,
"Binds": [
"test_data_bind:/app:rw,nocopy,tftp"
],
"BlkioDeviceReadBps": null,
"BlkioDeviceReadIOps": null,
"BlkioDeviceWriteBps": null,
"BlkioDeviceWriteIOps": null,
"BlkioWeight": 0,
"BlkioWeightDevice": null,
"CapAdd": null,
"CapDrop": null,
"Cgroup": "",
"CgroupParent": "",
"CgroupnsMode": "",
"ConsoleSize": [
0,
0
],
"ContainerIDFile": "",
"CpuCount": 0,
"CpuPercent": 0,
"CpuPeriod": 0,
"CpuQuota": 0,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpuShares": 0,
"CpusetCpus": "",
"CpusetMems": "",
"DeviceCgroupRules": null,
"DeviceRequests": null,
"Devices": null,
"Dns": null,
"DnsOptions": null,
"DnsSearch": null,
"ExtraHosts": [],
"GroupAdd": null,
"IOMaximumBandwidth": 0,
"IOMaximumIOps": 0,
"IpcMode": "",
"Isolation": "",
"Links": null,
"LogConfig": {
"Config": null,
"Type": ""
},
"MaskedPaths": null,
"Memory": 0,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
"NanoCpus": 0,
"NetworkMode": "test_default",
"OomKillDisable": false,
"OomScoreAdj": 0,
"PidMode": "",
"PidsLimit": null,
"PortBindings": {},
"Privileged": false,
"PublishAllPorts": false,
"ReadonlyPaths": null,
"ReadonlyRootfs": false,
"RestartPolicy": {
"MaximumRetryCount": 0,
"Name": ""
},
"SecurityOpt": null,
"ShmSize": 0,
"UTSMode": "",
"Ulimits": null,
"UsernsMode": "",
"VolumeDriver": "",
"VolumesFrom": null
},
"Hostname": "",
"Image": "nginx",
"Labels": {
"com.docker.compose.config-hash": "ca204030ebfa4e6e176ee2e383c1f0f9695fb76240141c92fd8d307909068d82",
"com.docker.compose.container-number": "1",
"com.docker.compose.depends_on": "",
"com.docker.compose.image": "sha256:a9dfdba8b719078c5705fdecd6f8315765cc79e473111aa9451551ddc340b2bc",
"com.docker.compose.oneoff": "False",
"com.docker.compose.project": "test",
"com.docker.compose.project.config_files": "/home/ubuntu/test/compose.yml",
"com.docker.compose.project.working_dir": "/home/ubuntu/test",
"com.docker.compose.service": "web",
"com.docker.compose.version": "2.29.1"
},
"NetworkingConfig": {
"EndpointsConfig": {
"test_default": {
"Aliases": [
"test-web-1",
"web"
],
"DNSNames": null,
"DriverOpts": null,
"EndpointID": "",
"Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAMConfig": null,
"IPAddress": "",
"IPPrefixLen": 0,
"IPv6Gateway": "",
"Links": null,
"MacAddress": "",
"NetworkID": ""
}
}
},
"OnBuild": null,
"OpenStdin": false,
"StdinOnce": false,
"Tty": false,
"User": "",
"Volumes": null,
"WorkingDir": ""
}
Notice this part:
"HostConfig": {
"AutoRemove": false,
"Binds": [
"test_data_bind:/app:rw,nocopy,tftp"
],
If it is a standard named volume it is set in Mounts instead of Binds
"Mounts": [
{
"Source": "test_data",
"Target": "/app",
"Type": "volume",
"VolumeOptions": {
"Subpath": "tftp"
}
}
],
So it turns out that the named volume backed by a bind mount is indeed handled differently which surprised me. And somehow the content of an object created from the volume
block is added after the mode which does not make sense so it must be a bug.
Even if it is a bug, I guess it was not discovered before, because a subpath could be useful when a container generates data ona volume, for example a document of a webapp is on this volume including the “photos” folder. Now you don’t need all the containers to use a named volume backed by a bind mount, only the first so it can copy the data to the volume, but the second container can use a simple bind mount and set any directory. This container then can for example generate thumbnails for the photos.
In case of a standard named volume, bind mounting anything from the docker data root could mean that if the volume is already deleted, and the short syntax is used for the bind mount definition, the fodler could be created in the docker data root and docker volume create
might fail when trying to create a volume with the same name later.