I needed Caddy to serve private files without world-readable permissions. The fix: a 4-line fstab entry. If you've ever tried to get a web server to serve files from a locked-down directory, you know the usual options are all bad. Chmod the home directory, run the server as root, or copy files around like it's 2005. At Tacavar we hit this exact wall: Caddy, running as uid 995, needed to serve video files from /home/tacavar/Desktop — a directory deliberately chmodded to 750. Caddy couldn't traverse the parent path. The solution wasn't loosening permissions. It was loosening path semantics with a bind mount. ## The Challenge: Caddy Can't Read chmod 750 Directories Caddy doesn't just need read permission on the file. It needs execute permission on every directory in the path from root to the file. /home is usually 755, fine. /home/tacavar is often 750, and /home/tacavar/Desktop is also 750. If you're running Caddy as its own user (you should), it falls outside the group ownership and gets nothing. And rightly so. The whole point of 750 is to keep other users out. You don't want to hand Caddy a key to your home directory just so it can serve one folder. The obvious fix — chmod 755 the home directory — is a security regression. The next tempting fix, running Caddy as root, is worse. We don't do that in devops. We don't do that anywhere. The real problem is that the Linux filesystem gives you permission control at the inode level, but path traversal is all-or-nothing. If you want Caddy to read a subtree, you have to grant it access to every parent. Or do you? ## Why Symlinks Don't Work Symlinks seem like the natural answer. Create a symlink from /var/www/videos to /home/tacavar/Desktop and be done. Except Caddy, and most web servers, resolve the full path before honoring symlinks — or they walk the real path during request handling. A symlink doesn't change the permission check; the kernel still resolves the target's parents. Caddy sees /var/www/videos, follows the link to /home/tacavar/Desktop, and then must traverse /home/tacavar. Same wall, different doorway. Some people try hardlinks. Those only work for files, not directories, and they break the moment you need to add new files. This is also where file permissions become a sticky mess: you'd need to maintain a separate mirror, update it on every write, and keep the inode references aligned. That's not devops, that's theater. ## The Bind Mount Solution: /etc/fstab to the Rescue The clean fix is a bind mount. A bind mount lets you attach an existing directory tree to another mount point. No copying, no ownership change, no permission loosening. You're telling the Linux filesystem: make this directory appear there as well. The original directory keeps its permissions. The new mount point gets its own path context — and if the parent directories of the new mount point are traversable by the web server, Caddy can serve the files. ``` /home/tacavar/Desktop /var/www/videos none bind,nofail,noexec 0 0 ``` Four lines? Technically one line in fstab, but let's be generous. The rest of the four-line bit is the mount command and the directory creation. After `sudo mkdir -p /var/www/videos` and `sudo mount -a`, the files in your private directory appear at the public path. Both paths point to the same inodes. When Hermes (our media processor) writes a file to /home/tacavar/Desktop, it's instantly available at hub.tacavar.com/videos/ — zero copy, zero delay. This works because the bind mount creates a new path that Caddy can traverse. The original `/home/tacavar` remains 750 and opaque. The new `/var/www/videos` parent directories are standard 755. Caddy's user can walk `/var/www/videos` just fine, and the kernel doesn't care that the underlying inodes also live under a private home directory. ## Security Without Data Duplication Let's talk about what this buys you from a security perspective. First, no data duplication. There's no cron job copying files, no rsync, no storage overhead. The bind mount is a namespace-level operation, not a data operation. The same inodes are visible in two places. If a file is encrypted at rest, it's encrypted in both paths. If you delete a file, it's gone from both paths. If you need to back up one directory, you're backing up the other — because they're the same data. Second, file permissions stay intact. The bind mount doesn't change ownership or mode. Your home directory remains 750. Caddy still cannot list /home/tacavar. It can only see the subtree exposed at /var/www/videos. This is a path-level permission boundary — exactly what you want for a multi-user or multi-service host. Third, you can combine this with `nofail` so the system doesn't hang on boot if the source mount isn't ready. Add `noexec` for good hygiene. The bind mount itself doesn't change the file's execution permissions, but it's a good habit for anything mounted near a web root. ## Production Lessons for File Serving Patterns Here's the part that generalizes beyond Caddy and home directories. We've used this pattern for more than personal files. Tacavar's deployment stack uses bind mounts to expose upload directories, build artifacts, and cache directories across containers and systemd services. The key principle: give each process the minimum path it needs, not the minimum inode it needs. The Linux filesystem is happy to present the same file through multiple paths with different permission contexts. That's a feature, not a hack. When you design file serving, think in terms of mount points rather than chmods. Ask yourself: does this service need a directory, or does it need a path? If Caddy needs to serve videos from a private home folder, you don't need to open the whole home folder. You need to present a new path that contains only what the web server is allowed to see. A bind mount does that in one line of fstab. The other lesson: don't confuse symlinks with permission bypasses. Symlinks are for aliasing, not for security. When a request comes in, the server walks the real path, and the kernel checks permissions on each component. Bind mounts, on the other hand, actually change the path that the kernel walks. That's why they work where symlinks fail. Finally, for TACAVAR-branded devops patterns, this is one small but repeatable technique. Everything we ship is built around minimal-infrastructure deployments where you don't run a storage service just to share a folder. A single Caddy instance with a carefully mounted filesystem can serve your public content and shield your private data at the same time. The next time you catch yourself about to `chmod 777` or run a web server as root, stop. Write a bind mount instead. It's four lines in fstab, and everything stays locked down. Explore Tacavar's devops patterns for secure, minimal-infrastructure deployments at tacavar.com.