A filesystem has two independent resources, and either can run out even while the other is plentiful.
A machine usually runs several filesystems: the root / has one, the training volume has its own, built inside an image file. Each keeps its own accounts, so you ask df about the path the program writes to.
A filesystem stores file contents in blocks, chunks of equal size. df -h counts blocks: Size is the total, Used is taken, Avail is what an ordinary user can still use. Any reserve for root is already left out of Avail.
Besides its contents, a file has a description: owner, permissions, modification time, the addresses of its blocks. That description lives in an inode, a separate record for every file and directory. The name is not in the inode: the directory holds it, as a "name → inode number" entry.
On ext4 the number of inodes is fixed when the filesystem is created and does not grow later. Every new file, even an empty one, takes one free inode. When none are free, the file is not created, and the system reports the same No space left on device.
| Resource | What uses it | How to check |
|---|---|---|
| blocks | file contents | df -h PATH, the Avail column |
| inodes | every file and directory, even an empty one | df -i PATH, the IFree column |
Hundreds of empty files can exhaust the inodes while blocks stay free, and one huge file can fill every block with a single inode. Ordinary disks have hundreds of thousands of inodes; the training volume deliberately has 256, so the shortage arrives in seconds.
In the exercise the service again cannot write its report, but the volume has been built in a different state.
What ran out this time
The service again cannot write its report to /mnt/lab-data. Try writing a line to report.txt, then compare df -h and df -i for the volume.