Skip to main content
TACAVAR
Build in Public

Serve Private Files via Caddy With a Bind Mount

Use a Linux bind mount to let Caddy serve files from a private home directory without chmod 755 or running as root. A 4-line /etc/fstab fix.

I needed Caddy to serve files from my home directory without making my home directory world-readable. The answer was a 4-line fstab entry.

The Problem: Caddy Can't Traverse chmod 750 Directories

Caddy runs as a non-root user (uid 995 on my system) and needs to serve files from /home/tacavar/Desktop/. That directory is chmod 750, meaning only the owner and group can read it. Caddy can't traverse the parent /home/tacavar/ because it's also 750. Even if the target directory is world-readable, Caddy fails at the parent. This is a classic Linux filesystem permission issue: the web server must have execute permission on every directory in the path. Symlinks don't help because Caddy resolves the full path and still hits the permission wall.

The Wrong Solutions: chmod, Privileged Mode, File Copies

Most operators reach for one of three bad fixes:

  • chmod 755 the home directory: Opens your entire home directory to the world. A single misconfigured file and you've leaked SSH keys or private docs.
  • Run Caddy as root or with CAP_DAC_OVERRIDE: Now any Caddy vulnerability gives an attacker full filesystem access. That's not DevOps, that's negligence.
  • Copy files to a public directory: Works but breaks real-time updates. If Hermes writes a new video every minute, you're either polling rsync or accepting stale data. Both waste CPU and disk.

None of these scale. You need a solution that respects file permissions, doesn't duplicate data, and keeps your home directory private.

The Right Solution: Bind Mount in /etc/fstab

A bind mount remounts a directory tree at another point in the filesystem. It's a single line in /etc/fstab:

/home/tacavar/Desktop /var/www/videos none bind,noexec,nosuid,nodev,uid=995,gid=995 0 0

That's it. Four fields. After mount -a, Caddy sees /var/www/videos as a regular directory with proper ownership. The original /home/tacavar/Desktop remains 750. Caddy never needs to traverse the home directory.

How It Works: Same Inodes, Different Paths

A bind mount doesn't copy data. Both paths point to the same inodes on the Linux filesystem. When Hermes writes a file to /home/tacavar/Desktop/video.mp4, it instantly appears at /var/www/videos/video.mp4. No copy, no sync, no delay. The kernel handles the mapping at the VFS layer. This is the same mechanism that powers container mounts and chroot jails. It's been in the kernel since 2.4, but most DevOps engineers forget it exists.

Security and Performance Benefits

  • No permission changes: Your home directory stays 750. Caddy gets exactly the subtree it needs.
  • No privileged processes: Caddy runs as uid 995 with no extra capabilities.
  • Zero copy: Files are served directly from the original location. No disk waste, no I/O overhead.
  • Instant propagation: Any write by Hermes is immediately visible to Caddy. No polling, no cron jobs.

The bind mount also supports noexec, nosuid, and nodev options, further hardening the mount point. You're not just solving a permission problem—you're applying defense in depth.

Real-World Use: Hermes Writes, Caddy Serves Instantly

At Tacavar, Hermes processes user uploads and writes them to /home/tacavar/Desktop/. Caddy serves them at hub.tacavar.com/videos/. The bind mount makes this seamless. When a new video lands, it's available immediately. No file permissions errors, no stale copies, no security holes. This pattern handles hundreds of files per minute without breaking a sweat.

Generalizable Pattern for Any Web Server

This isn't Caddy-specific. Any web server—nginx, Apache, even a Python dev server—can benefit from bind mounts. The pattern is:

  1. Keep sensitive data in private directories with restrictive permissions.
  2. Create a bind mount in /etc/fstab that exposes only the needed subtree.
  3. Point your web server at the mount point.

You can apply this to log directories, cache folders, or any data that needs to be served without compromising the parent directory. It's a fundamental DevOps trick that separates concerns at the filesystem level.

For more DevOps patterns that keep your infrastructure secure and simple, visit tacavar.com/devops.