Caddy Couldn't Read My Home Directory. A 4-Line fstab Entry Fixed It.
Caddy uid 995 needed files from chmod 750 /home. I used one bind mount, zero copies.
Tacavar's Hermes video pipeline writes raw cuts and rendered outputs into a directory under /home/tacavar. The Caddy web server runs as uid 995. The home directory is chmod 750. That combination produces a hard permission wall: Caddy can't traverse /home/tacavar, so it can't reach any file below it—even if the file itself is world-readable. The fix is not another chmod. It's a caddy bind mount that changes the path, not the ownership.
The Permission Wall: Caddy uid 995 vs chmod 750 Home
Linux file permissions are evaluated per path component. To open /home/tacavar/Desktop/video.mp4, Caddy needs execute (x) permission on /, /home, /home/tacavar, and /home/tacavar/Desktop. The file's mode is irrelevant until traversal succeeds. On Tacavar's host, /home/tacavar is drwxr-x--- (750). Owner tacavar has full access. Group has read/execute. Caddy, uid 995 and not in that group, gets nothing. It cannot list the directory and cannot traverse into it. The kernel returns EACCES before Caddy ever looks at the video file.
This is correct security posture. A home directory should not be world-readable. The mistake is assuming Caddy needs to live inside that posture. It doesn't. It needs a path it can traverse. The goal is to serve files from home directory without opening the home directory.
Why Symlinks and chmod Fixes Fail
The first instinct is a symlink: point /var/www/videos at /home/tacavar/Desktop. That fails for the same reason. A symlink is not a permission bypass. When Caddy follows /var/www/videos/video.mp4, the kernel resolves the target path and checks permissions on every real directory. It still hits /home/tacavar and stops. Caddy walks the full path; symlinks just give it a longer route to the same wall.
The second instinct is chmod o+x /home/tacavar or chmod 755 /home/tacavar. That lets Caddy traverse, but it also lets every local uid do the same. You've traded a private home directory for a public path. Running the caddy web server as root is worse: one bug in a handler or module and the entire filesystem is in scope. Copying files into /var/www/videos works, but now Hermes writes to one path and Caddy reads another. You need rsync, cron, inotify, or a queue. Every copy adds latency, disk usage, and a new failure mode. For a video pipeline, that is pure waste.
The 4-Line fstab Bind Mount
The clean fix is an fstab bind mount. It makes one directory appear at another path. Both paths point to the same inodes. No data is copied. No ownership changes. Caddy still runs as uid 995. The home directory stays 750.
Four lines, run once on the host:
sudo mkdir -p /var/www/videos
echo '/home/tacavar/Desktop /var/www/videos none bind 0 0' | sudo tee -a /etc/fstab
sudo systemctl daemon-reload
sudo mount -a
The first line creates the mount point. The second adds the fstab bind mount entry. The third reloads systemd so it sees the updated fstab. The fourth mounts everything without a reboot. From that point on, /var/www/videos is /home/tacavar/Desktop. The parent /home/tacavar is still 750, but Caddy never traverses it. It enters through /var/www, which is already world-traversable, and lands directly inside the Desktop directory. If you want to keep Caddy read-only, change the fstab options to bind,ro. Hermes can still write to the source path; Caddy can only read through the mount.
Zero-Copy File Serving at hub.tacavar.com/videos
Caddy config becomes trivial:
hub.tacavar.com {
root * /var/www/videos
file_server browse
}
When Hermes finishes writing a 12 GB render to /home/tacavar/Desktop/render-042.mp4, the same inode is immediately visible at /var/www/videos/render-042.mp4. Caddy serves it at hub.tacavar.com/videos/render-042.mp4 with zero copy, zero sync delay, and zero additional storage. There is no second source of truth. There is no cache to invalidate. The file exists once. The caddy bind mount is just a second path to it.
This matters for operator velocity. A copy-based pipeline needs a job queue, retries, and cleanup. A bind-mount pipeline needs an fstab line. When you are shipping internal tools at Tacavar, removing moving parts is the feature.
Security Tradeoffs: Path Boundaries Without Ownership Changes
A bind mount is not magic. It is a path-level permission boundary. Caddy can read everything under /var/www/videos because that mount points to the Desktop directory. It cannot walk up to /home/tacavar, /home/tacavar/.ssh, or other siblings. Those paths are not under the mount. If Caddy is compromised, the blast radius is the mounted directory, not the entire home directory.
That is a better tradeoff than chmod'ing home. You keep linux file permissions intact. You keep service isolation. You avoid privileged processes. You also get a declarative configuration in /etc/fstab, so the mount survives reboots and is visible to configuration management. The only caveat: the mounted directory must be traversable by Caddy. If the Desktop directory itself is 750, either adjust its group or use an ACL for uid 995. But the home parent remains locked down. The mount bypasses the parent, not the target's own mode.
Reusing the Pattern for Other Private Directories
Once you see the shape, you can reuse it. Any service that needs serve files from home directory can use the same caddy bind mount. Private build artifacts under /home/deploy/output can appear at /var/www/artifacts. Customer uploads under /home/tacavar/incoming can appear at /var/www/uploads. The pattern is always the same: create a mount point under /var/www, add an fstab bind mount line, run mount -a, and point the caddy web server at the new path. Ownership stays unchanged. Copies stay at zero.
For Tacavar, this is now the standard escape hatch for private directories. Hermes writes where it already has permissions. Caddy reads from a path it can traverse. The two never need to negotiate ownership. That is the whole point.
Use the same bind-mount pattern in Tacavar's Hermes video pipeline at tacavar.com/videos.