Aad Versteden a9349d3107 Initial import
2026-09-22 10:55:10 +02:00
2026-09-22 10:55:10 +02:00
2026-09-22 10:55:10 +02:00
2026-09-22 10:55:10 +02:00

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'.

Description
Update a mountpoint with live mounts
Readme 30 KiB
Languages
C 56.3%
Shell 33.8%
Makefile 9.9%