Tech Digest

Guide

Why does a Docker container write to the empty folder when the NFS or USB drive is not mounted yet?

A mount point with nothing mounted on it is an ordinary empty directory on the root disk, and the Docker daemon starts restart-policy containers without waiting for any NFS share or `nofail` disk. The container writes to that directory, the drive mounts over it later, and the files vanish from view. We took the scenario apart piece by piece to see which fix actually stops it.

Last reviewed

Why does a Docker container write to the empty folder when the NFS share or USB drive is not mounted yet?

Because an unmounted mount point is a real, empty directory on the root disk, and Docker bind-mounts whatever is at that path when the container starts. The stock docker.service is not ordered after NFS mounts at all, and a disk with nofail in fstab is not ordered before anything, so containers with a restart policy start before the mount or without it. Add a drop-in to docker.service with RequiresMountsFor=/path in the [Unit] section: Docker then waits for the mount and refuses to start if it fails. For a single container, chattr +i on the empty mount point turns the silent write into Operation not permitted.

Because a mount point with nothing mounted on it is an ordinary empty directory on the root disk, and Docker bind-mounts whatever is at that path when the container starts. At boot the daemon starts every container with a restart policy as soon as it is up, and the stock docker.service waits for neither NFS shares nor disks marked nofail in fstab. The container writes into the root disk's /mnt/media. When the drive mounts later, it covers that directory, and the files vanish from view while still using root disk space.

Docker's bind mount page uses this exact picture: files saved into /mnt, then a USB drive mounted there, are "obscured by the contents of the USB drive until the USB drive was unmounted".

Two answers circulate, and neither touches the cause#

The first answer is to fix it in Compose: depends_on, a healthcheck, or a sleep in the entrypoint. None can work. Compose is not running at boot, and the Docker docs say restart policies start containers "when Docker restarts". Our depends_on guide read the daemon's restore code: it starts restart-policy containers in parallel and waits for nothing on the host.

The second answer is a systemd drop-in with RequiresMountsFor=, which is right, but a widely copied snippet puts it under [Service]. systemd lists it among the [Unit] section options. Under [Service] it is ignored with a warning nobody reads, and the race stays where it was.

What we measured#

We ran each case on Docker Engine Engine 28.1.1 with Compose 2.35.1 on Ubuntu 20.04 with systemd 245 and kernel 5.4. The "USB disk" was a 200 MB ext4 image on a loop device, so detaching the loop device is unplugging the disk. The "blocked NFS server" was an NFSv4 export at 192.0.2.10, a documentation address that never answers. We did not reboot the host; systemd's side was tested with stand-in services carrying the directives a docker.service drop-in would.

CaseWhat happened
Container started with the disk unplugged50 MB written to the empty mount point, on the root logical volume
Disk then mounted on the hostHost saw the disk; the running container still saw the root disk folder and kept writing there
Disk unmounted againThe 50 MB file and two small files reappeared on the host
Bind mount with :rslaveContainer saw the disk as soon as it mounted, but had already written before that
chattr +i on the empty mount pointtouch: /media/x: Operation not permitted in Alpine and Debian images
RequiresMountsFor= in [Unit], disk absentWaited 90 s, Timed out waiting for device, service not started, nothing written
Same, disk attached 10 s inService started at +10 s and wrote to the disk
RequiresMountsFor= under [Service]Unknown key name ... ignoring, started at once, wrote to the root disk
WantsMountsFor= on systemd 245Unknown key name ... ignoring, started at once, wrote to the root disk
RequiresMountsFor= on the NFS pathMount timed out after 90 s, service not started
Docker local driver NFS volumeconnection timed out after 180 s, container not started
x-systemd.automount, disk absentContainer start failed after 90 s with no such device

Why the late mount does not reach the running container#

Docker's default bind propagation is rprivate, which its docs describe as "no mount points anywhere within the original or replica mount points propagate in either direction". The container's view of the path was fixed at start, and a mount made on the host afterwards never reached it.

:rslave changes that: the late mount showed up inside the container immediately. It still does not fix this problem. Everything the app wrote before the mount, a fresh database, a library index, a first batch of downloads, stays on the root disk, and the app now sees a disk that does not have it.

On a small root disk this shows up later as a full disk that measuring /var/lib/docker cannot explain, because the space is under the mount point.

nofail is what removed the wait#

Without nofail, systemd orders a local fstab mount before local-fs.target, and normal services, Docker included, start after it. With it, systemd's docs say the mount is "only wanted, not required" and "not ordered before these target units", so boot continues "without waiting for the mount unit".

We ran systemd's fstab generator on two lines for the same disk to see the difference:

ini
# defaults                   -> local-fs.target.requires/srv-usb2.mount
Before=local-fs.target
# defaults,nofail            -> local-fs.target.wants/srv-usb.mount
# (no Before= line at all)

People add nofail to a USB disk for a good reason: without it, the boot requires a disk that may not be there. Keep it, and put the ordering back on the one service that needs it.

NFS shares are not waited for even without nofail#

Network mounts are ordered before remote-fs.target, not local-fs.target. The docker.service in Moby's repository has After=network-online.target nss-lookup.target docker.socket firewalld.service containerd.service time-set.target and nothing about mounts. systemd gives services an implicit After=basic.target, and the systemd.special page says basic.target should pull in "all local mount points". On our host, systemctl list-dependencies --after docker.service contained local-fs.target through that chain and no remote-fs.target anywhere.

So on a stock install Docker can start before an NFS share in fstab whether or not it has nofail.

The fix: make docker.service require the mount#

bash
sudo systemctl edit docker.service
ini
[Unit]
RequiresMountsFor=/mnt/media /mnt/nas

Restart Docker and confirm with systemctl show docker -p Requires -p After, which should now list mnt-media.mount. systemd says the option adds Requires= and After= for every mount unit needed to reach the path, and still pulls in a noauto mount.

Know what you are buying. With Requires= plus After=, systemd says that if the required unit fails, "this unit will not be started": the whole Docker daemon, not one container. Our stand-in waited 90 s for the absent disk, or 90 s for the unreachable NAS, then stayed down. Every stack on the host waits for that disk.

Also measured: systemctl stop on the mount unit stopped the service that required it; a plain umount did not. Where an unplugged backup disk is normal, use a per-container guard instead.

WantsMountsFor= fixes the race but not the absent disk#

systemd 256 added WantsMountsFor=, "same as RequiresMountsFor=, but adds dependencies of type Wants= instead of Requires=". Docker still waits for the mount to finish, so a slow NAS no longer loses the race, but a failed mount no longer blocks Docker, which then starts the container on the empty directory. It solves timing only. On systemd 245 it was ignored as an unknown key, so check systemctl --version before relying on it. We did not test it on 256.

chattr +i: a guard for one mount point#

When the right answer for one disk is "fail that container, leave the rest alone", mark the empty mount point immutable while nothing is mounted on it:

bash
findmnt /mnt/media || sudo chattr +i /mnt/media
lsattr -d /mnt/media     # ----i--------- /mnt/media

Writes to the empty directory then fail with Operation not permitted, even from root inside the container, so the failure shows up in the app's log instead of on the root disk. The disk still mounted over the immutable directory, and writes to the mounted disk worked, because the flag lives on the root disk's directory, not on the drive.

Run it on a mounted path and you flag the drive's top directory instead: our writes to the drive itself were refused. The container still starts, and reading an empty directory is not an error, so an app that only reads the path is not stopped by it.

Which guard to use#

SituationUseWhat happens when the mount is missing
NAS or disk every service depends onRequiresMountsFor= drop-inDocker waits, then does not start
Slow NAS, systemd 256 or newer, absence is fineWantsMountsFor= drop-inDocker waits, then starts on the empty folder
One removable disk, one containerchattr +i on the mount pointOnly that container's writes fail
NFS share used by one stackDocker local driver NFS volumeThat container fails to start
Mount on first accessx-systemd.automountContainer start fails after the device timeout

One guard that does not help: create_host_path: false in Compose and --mount type=bind refuse a bind source that "does not exist", and an unmounted mount point exists. Our bind mount guide covers the case where it does not, and the root-owned folder Docker creates then.

Clean up what already landed#

Stop the containers using the path, unmount the drive, and look:

bash
sudo umount /mnt/media
sudo du -sh /mnt/media; ls -la /mnt/media

Anything there was written while the drive was absent. Copy what is worth keeping onto the drive, delete the rest, then mount again and set a guard. If it is an app's database, decide which copy is current rather than merging files; Why SQLite says database is locked on an NFS or SMB share explains why app config belongs on a local disk in the first place.

When this answer stops applying#

  • Rootless Docker runs as a user service, not docker.service, so the drop-in above does not apply to it. We did not test it.
  • Docker Desktop was not tested; Docker's docs say mount propagation does not work there.
  • Other filesystems and servers. chattr +i was tested on ext4 only. Our NFS server never answered; a server that is up but refuses the export was not tested.
  • systemd 256 and later were read from the documentation, not tested.

What to do next#

Run findmnt for every bind mount source that lives on another disk or share. For each one, decide whether Docker itself should wait for it, and add RequiresMountsFor= in [Unit] if so, or chattr +i on the empty mount point if only one container depends on it. Then check the root disk for files hidden under mount points. If the disk is part of a larger layout decision, ZFS, btrfs, mdadm or one disk covers which filesystem to put under it.

Questions#

Where do I put RequiresMountsFor for Docker?

In the [Unit] section of a drop-in: run sudo systemctl edit docker.service, add [Unit] and RequiresMountsFor=/mnt/media on the next line, save, then restart Docker. systemd's own documentation lists the option under [Unit]. Under [Service] it does nothing: systemd 245 logged Unknown key name 'RequiresMountsFor' in section 'Service', ignoring. and our stand-in service started at once and wrote to the root disk.

Does nofail cause Docker to write to the root disk?

It removes the wait that would have prevented it. systemd's mount documentation says a nofail mount is only wanted by local-fs.target or remote-fs.target and is not ordered before them, so boot carries on without waiting and whether or not the mount works. In our generator test the nofail line lost its Before=local-fs.target line. Docker is ordered after local-fs.target, so without nofail it would have waited. Keep nofail and add RequiresMountsFor= to docker.service.

Does depends_on or a restart policy wait for an NFS mount?

No. depends_on is read by Compose, which is not running when the host boots, and it orders containers, not host mounts. The Docker docs say restart policies start containers "when Docker restarts", so every container with always or unless-stopped is started by the daemon as soon as the daemon is up. Waiting for a mount has to happen one level lower, in systemd, before the daemon starts.

Will chattr +i on the mount point stop the drive from mounting?

No. In our test a disk mounted normally over a directory marked immutable, and writes to the mounted disk worked, because the flag belongs to the directory on the root disk, not to the filesystem mounted over it. Set it while nothing is mounted. Run it on a mounted path and you mark the drive's own top directory immutable instead, which blocked writes to the drive itself.

How do I find files a container wrote under the mount point?

Stop the containers using the path, unmount the drive or share, and list the directory: the files are on the root disk, hidden by the mount. In our test a 50 MB file and two small ones reappeared after umount. Move them onto the mounted drive or delete them, then mount again. findmnt /mnt/media before and after confirms what is mounted where.

What is WantsMountsFor and should I use it instead?

It is the same as RequiresMountsFor= but adds a Wants= dependency instead of Requires=, and it appeared in systemd 256. Docker still waits for the mount to finish, but if the mount fails Docker starts anyway, so the container writes to the empty folder again. It fixes a slow mount, not a missing disk. On older systemd, such as 245 on Ubuntu 20.04, it is ignored with an unknown key warning.

Sources#

Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.