Skip to main content
TACAVAR
•Build in Public

How I Served Private Home Directory Files Through Caddy With 4 Lines of fstab

Caddy couldn't traverse a chmod 750 home directory. Tacavar's 4-line fstab bind mount serves private files with zero chmods and zero copies.

Caddy couldn't reach my home directory. The fix was 4 lines in fstab, zero chmods.

At Tacavar, our media pipeline has a writer process — Hermes — that drops finished files into /home/tacavar/Desktop/, and hub.tacavar.com needs to serve them publicly. Two sane defaults collided: a home directory locked to 750, and a Caddy process running as uid 995 that the kernel refused to let anywhere near it. If you're building self-hosted infrastructure, you will hit this exact wall. Here's what doesn't work, and the caddy bind mount pattern we shipped instead.

The Problem: chmod 750 and a Web Server That Can't Traverse It

Caddy on our droplet runs the way it should: a dedicated system user (uid 995), no shell, no login, managed by systemd. /home/tacavar is chmod 750, owned by tacavar:tacavar. Linux requires the execute bit on every directory component of a path just to traverse it — read to list the final component, execute to open files inside. /home is 755, so that hop is fine. /home/tacavar at 750 is where resolution dies: uid 995 has no entry in the owner/group triad, traversal is denied, and Caddy returns 403s no matter how the site handler is configured. The target directory could be 755 and it wouldn't matter — the parent kills the walk.

The naive reading of this is "Caddy can't read the file." The accurate reading: the kernel won't let Caddy traverse the path. That distinction matters, because most fixes people reach for are aimed at the wrong layer.

Why Symlinks and Permission Changes Both Fail

Attempt one: symlink /var/www/videos -> /home/tacavar/Desktop/. Doesn't work. Path resolution follows the symlink and then keeps walking the target's components — Caddy still traverses /home/tacavar, still hits the 750, still denied. A symlink is a pointer, not a permission grant.

Attempt two: loosen the home directory to 755. Now every local account on the box can list my home directory. On a multi-user system that's a real security regression, and even on a single-tenant droplet it's a habit I refuse to build a stack on.

Attempt three: run Caddy as tacavar, or as root. Worse. A root-running web server means any misconfiguration or RCE is a full compromise of the box, and I'd be changing a service identity to work around a path problem — backwards.

Attempt four: have Hermes copy or rsync its output into /var/www/videos. This works, and it's the wasteful default. Storage doubles, a sync delay appears between write and serve, and there are now two sources of truth that can drift.

All four fail the same test: they either widen the blast radius or duplicate the data.

The 4-Line fstab Bind Mount That Fixed It

The actual fix — one fstab entry, one mkdir, one mount, one reload:

# /etc/fstab
/home/tacavar/Desktop  /var/www/videos  none  bind  0  0
mkdir -p /var/www/videos
mount -a
systemctl reload caddy

That's it. The Desktop directory now appears at /var/www/videos, a path whose parents Caddy can already traverse, and files Hermes writes show up instantly at hub.tacavar.com/videos/. No ownership changes, no sync job. For boot reliability, let fstab ordering or an x-systemd.automount unit handle mount timing before Caddy starts — but the core of the fix is genuinely one line.

When people ask about fstab bind mount permissions, the mental model to hold is this: the bind mount doesn't weaken anything at the source path. It adds a second path to the same directory, and you decide which path the service uses.

How Both Paths Point to the Same Inodes

A bind mount is not a copy, and it's not a symlink. It's a second directory entry resolving to the same inode. Run stat on both paths — identical device and inode numbers. Hermes writes to /home/tacavar/Desktop/clip.mp4; Caddy serves /var/www/videos/clip.mp4; there is exactly one copy of those bytes on disk, one inode, zero lag. df confirms nothing new was allocated. Delete the file from either path and it's gone from both, because there was never a "both" — there's one object with two names.

This is the property that makes the pattern more than a permissions hack: it's the same primitive that lets Tacavar's pipeline move data between producer and publisher without a single duplication step.

Security Properties: Path-Level Boundaries Without Copies

What did we actually expose? One subtree. The home directory stays 750; every other path under /home/tacavar is as unreachable to uid 995 as before. Caddy got a handle to exactly the directory we chose and nothing else. Unmount, and the exposure disappears instantly — no cleanup of copied files, no stale replicas. Compare that to the alternatives: chmod 755 exposes the whole tree, a privileged Caddy exposes the whole box, and an rsync job exposes a second copy that lives forever.

The boundary is drawn at the path level, by you, explicitly, in a file auditable with cat /etc/fstab. For anyone who treats linux web server file permissions as an actual security model rather than an afterthought, that explicitness is the point.

When to Use Bind Mounts Instead of the Alternatives

Use a bind mount to serve files from home directory paths when all of these hold: the writer owns its output location and you can't or shouldn't change it; the consumer is a local, lower-privilege service; and duplicating the data would cost real storage or introduce drift.

Skip it when: the consumer is on another machine — bind mounts are kernel-local to one box, so use NFS or object storage; you need different permission semantics at the two paths — bind shares the inode, so modes travel with it; or you're in containers, which already give you mount namespaces and cleaner tools for the same job.

The pattern is small, reversible, and honest about what it's doing — which is more than I can say for a 755 home directory.

Need infrastructure that moves data without duplicating it? Tacavar runs its entire media pipeline on two self-hosted droplets. Explore our infrastructure patterns at tacavar.com/infrastructure.