#+TITLE: OpenCode live mounts #+STARTUP: :indent 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: #+begin_src bash make make install sudo chown root ~/opencode-workspace/src/mountctl sudo chmod 4755 ~/opencode-workspace/src/mountctl #+end_src 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: #+begin_src yaml services: app: image: ubuntu volumes: - type: bind source: /home/you/opencode-workspace/workspace target: /workspace bind: propagation: rslave entrypoint: "tail -f /dev/null" #+end_src #+begin_src bash docker compose up -d #+end_src Now mount something: #+begin_src bash ~/opencode-workspace/mount-opencode /home/you/code/elm #+end_src It shows up at =/workspace/elm= inside the running container. That's it. * How-to guides ** Add a mount #+begin_src bash ~/opencode-workspace/mount-opencode [name] #+end_src =name= defaults to =basename =. The mount is recorded in =mounts.list=. ** Remove a mount #+begin_src bash ~/opencode-workspace/unmount-opencode #+end_src ** Recover all mounts after a reboot Bring the container back up first (its rslave bind must be in place), then: #+begin_src bash ~/opencode-workspace/recover-mounts #+end_src 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) #+begin_example mountctl mount # bind-mount source at STAGING_ROOT/name mountctl umount # umount and rmdir the mountpoint #+end_example =STAGING_ROOT= is baked in at compile time; default is =$(HOME)/opencode-workspace/workspace=. Override at build time: #+begin_src bash make STAGING_ROOT=/some/other/path #+end_src ** =mounts.list= Plain text, one mount per line, tab-separated: #+begin_example \t #+end_example 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'=.