OpenCode live mounts
Bind-mount host folders into a running Docker container, add and remove them at will, and re-apply them after a reboot. No daemon, no Docker API tricks — one setuid helper and three shell scripts.
Tutorial: get started
Build and install the helper and scripts:
make
make install
sudo chown root ~/opencode-workspace/src/mountctl
sudo chmod 4755 ~/opencode-workspace/src/mountctl
Give the container a window into the staging area. Add this volumes
block to your service (replace source with the absolute path to your
staging directory) and bring it up once:
services:
app:
image: ubuntu
volumes:
- type: bind
source: /home/you/opencode-workspace/workspace
target: /workspace
bind:
propagation: rslave
entrypoint: "tail -f /dev/null"
docker compose up -d
Now mount something:
~/opencode-workspace/mount-opencode /home/you/code/elm
It shows up at /workspace/elm inside the running container. That's it.
How-to guides
Add a mount
~/opencode-workspace/mount-opencode <source> [name]
name defaults to basename <source>. The mount is recorded in
mounts.list.
Remove a mount
~/opencode-workspace/unmount-opencode <name>
Recover all mounts after a reboot
Bring the container back up first (its rslave bind must be in place), then:
~/opencode-workspace/recover-mounts
It re-runs every mount listed in mounts.list; safe to run anytime.
Explanation
Docker has no way to add a mount to a running container, and nsenter
mounts can't see host paths that aren't already bind-mounted in. The
rslave propagation on the staging directory is the bridge: it is the
one path shared between host and container namespaces, and any sub-mount
created on the host under it propagates into the container. rslave
is one-way (host → container), so container-side mounts don't leak back
onto the host.
The staging directory must therefore exist before the container starts, and the container must be recreated (a brief restart) if it was started without the bind. This is a Docker limitation, not a tool choice.
Why setuid root: mount(2) requires CAP_SYS_ADMIN, which ordinary
users don't have. A compiled C helper is the only thing Linux honours
setuid on (scripts are ignored). The helper does only raw bind mounts
and umounts — no shell, no system() — and rejects names that could
escape the staging root (= /=, leading dot, empty) or that point at an
existing non-directory (symlink-escape guard).
Reference
mountctl (the setuid helper)
mountctl mount <source> <name> # bind-mount source at STAGING_ROOT/name mountctl umount <name> # umount and rmdir the mountpoint
STAGING_ROOT is baked in at compile time; default is
$(HOME)/opencode-workspace/workspace. Override at build time:
make STAGING_ROOT=/some/other/path
mounts.list
Plain text, one mount per line, tab-separated:
<source>\t<name>
Managed by the scripts; edit by hand if you must.
Makefile targets
make |
compile src/mountctl |
make install |
deploy to ~/opencode-workspace/ and create the symlinks |
make clean |
remove the binary |
Authorization note
The helper is setuid root (mode 4755) for now. TODO: restrict to a
live-mount Unix group via getgroups(2). If your home filesystem is
mounted nosuid the setuid bit is silently ignored — check with
mount | grep 'on /home'.