Three errors. Two container recreations. Each error invisible until the one above it was peeled away.
I had two Splatoon manga volumes — CBZ files, already tagged and ready — and I wanted them on the shelf in Booklore. The whole point of BookDrop is that you don’t think about it. You drag files into a folder. The app notices, imports them, sorts them by series and volume number. Thirty seconds, maybe. I’d done it with local storage dozens of times without incident.
These files were landing on a NAS-mounted CIFS share. That shouldn’t matter. A volume is a volume is a volume. I dropped the files in, waited, and watched nothing happen. The logs told me why — or rather, they told me the first of three whys.
What I expected: Drag two files into BookDrop. Auto-import picks them up, shelves them. Done in thirty seconds.
What actually happened: Three sequential permission failures, each invisible until the one above it was fixed. An hour of container lifecycle management for a drag-and-drop operation.
Layer 1: The Obvious One
Read-only file system
Fair enough. The nas_comics volume was mounted with :ro in docker-compose. I’d originally set it read-only because Booklore was just reading and indexing — it didn’t need to write. BookDrop changes the deal. Auto-import means moving files from the drop folder into the library structure. Writing is the whole point.
volumes:
- /mnt/nas/comics:/comics:ro
- /mnt/nas/comics:/comics
Removed :ro. Recreated the container. Dropped the files in again.
Fixed. Tested. Broke again.
That felt premature. It was.
Layer 2: The UID Mismatch
Access Denied
Different error. Progress? Maybe. The kind of progress where the mountain doesn’t get shorter, it just changes color.
Booklore’s Java process runs as uid=501(booklore). The CIFS mount is owned by uid=1000 with 755 permissions. The container user literally cannot write to a directory owned by a different user with no group or world write permissions.
This is one of Docker’s most common traps and it’s invisible when a read-only flag is sitting on top of it. The :ro mount was absorbing all the write failures. Remove the obvious restriction and the subtle one appears. I couldn’t have found this bug without fixing the first one — which makes the first fix feel useful and the second fix feel like the real answer.
driver_opts:
type: cifs
device: "//nas/comics"
o: "username=...,password=...,file_mode=0777,dir_mode=0777"
Setting 0777 on a CIFS mount feels wrong — it’s not actually changing filesystem permissions on the NAS, it’s telling the mount driver to present everything as world-writable to the container. The NAS still has its own ACLs. But it’s the standard fix for the UID mismatch problem with CIFS volumes in Docker.
This required removing the volume entirely and recreating it. Docker doesn’t let you change volume options on an existing named volume. So: docker-compose down, docker volume rm, edit the compose file, docker-compose up -d. Container recreation number two.
Dropped the files in. Waited. Got that little hit of confidence that comes from having already been wrong once and surely not being wrong again.
Fixed. Tested. Broke again.
Layer 3: The Upstream Bug
Operation not permitted
Third error. Third personality. At this point I’m not even surprised. I’m just reading the next page.
Booklore’s auto-naming generates directory paths from manga metadata — series name, volume number, reading order. For manga, this produces paths like 06. Splatoon/ with a trailing dot and space. Perfectly legal on ext4. Illegal on SMB shares.
The CIFS filesystem itself was refusing to create the directories Booklore wanted to create. Not a permission problem. Not a configuration problem. A genuine incompatibility between Booklore’s naming logic and network storage path restrictions.
| Layer | Error | Cause | What It Hid |
|---|---|---|---|
| 1 | Read-only file system | :ro mount flag | UID mismatch (Layer 2) |
| 2 | Access Denied | Container UID ≠ mount UID | SMB path bug (Layer 3) |
| 3 | Operation not permitted | Illegal characters in SMB paths | Nothing — this was the bottom |
No configuration fix for layer three. I manually placed the files with clean filenames and moved on. This is a Booklore bug — their auto-naming generates filenames that are illegal on network storage. Anyone running comics on a NAS will hit it eventually.
The Psychology of Layered Bugs
The temptation after fixing layer one is to drop the file in, see it work, and walk away. That’s exactly what I did — twice. Each fix felt like the fix. The read-only flag was obvious. The UID mismatch was a known Docker pattern. Each one produced a satisfying moment of “ah, that was it” followed by the same two files sitting in BookDrop, unprocessed.
Why layered permission bugs are particularly deceptive
Permission layers evaluate in order. First “no” wins. Every “no” after that is silent — it never gets reached. So you fix one thing, it works, you feel smart, and then it breaks for a completely different reason you couldn’t have seen until the first one was cleared.
The total number of layers is unknown until you’ve peeled them all. Each fix creates false confidence that scales with the number already fixed. The countermeasure is asking “what else could say no?” after every fix, not just “does it work now?”
The Damage Report
| Metric | Value |
|---|---|
| Expected time | ~30 seconds |
| Actual time | ~1 hour |
| Container recreations | 2 |
Full down → volume rm → edit → up cycles | 2 |
| Distinct error messages | 3 |
| Layers that were fixable via configuration | 2 of 3 |
| Files successfully auto-imported | 0 |
Two CBZ files. Zero of them made it in through the intended workflow. The final resolution was manual placement with hand-cleaned filenames — the thing BookDrop exists to eliminate.
Pattern Recognition
The pattern isn’t Docker-specific. It’s every system where multiple gatekeepers evaluate in sequence and only the first rejection is visible:
- Firewall rules in front of app-level auth in front of database row security
- Nginx returning 403 before your app sees the request — which would also 403, for a different reason
- CORS preflight failing before the auth header gets checked, so you fix CORS and discover your token is expired too
The structure is always the same: fix, false confidence, repeat. The fix for layer one is never the fix for the problem — it’s the fix for the first problem. Layers two and three are patient.