Initial import

This commit is contained in:
Aad Versteden
2026-09-22 10:55:10 +02:00
commit a9349d3107
6 changed files with 330 additions and 0 deletions

133
README.org Normal file
View File

@@ -0,0 +1,133 @@
#+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 <source> [name]
#+end_src
=name= defaults to =basename <source>=. The mount is recorded in
=mounts.list=.
** Remove a mount
#+begin_src bash
~/opencode-workspace/unmount-opencode <name>
#+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 <source> <name> # bind-mount source at STAGING_ROOT/name
mountctl umount <name> # 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
<source>\t<name>
#+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'=.