134 lines
3.7 KiB
Org Mode
134 lines
3.7 KiB
Org Mode
#+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'=.
|