docs/slocker-lite.1.scd covers the full CLI (grouped like README.md's flag table, including the DEBUGGING section for --mount/--umount/ --cleanup and the reassigned-short-option note), FILES, EXIT STATUS, and SEE ALSO. meson.build builds it into slocker-lite.1 and installs it under man1 only when scdoc is found on the host -- configuration still succeeds without it, matching this project's existing "degrade gracefully when an optional tool is missing" policy. Verified the rendered page with `man --warnings` (clean, no troff warnings) and a DESTDIR install. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
32 KiB
slocker-lite
slocker-lite mounts an OCI Image Layout
tar (the format produced by skopeo, podman save --format oci-archive, or a modern
docker save) and runs a sandboxed command against it — without podman, docker,
or kernel overlayfs.
Why
The target environment is Android with a stock kernel: podman/docker don't run
there (missing namespace support), and there's no kernel overlayfs. slocker-lite
works around both: it imports image layers into containers-storage and mounts them
with fuse-overlayfs (userspace, no kernel overlayfs needed), then sandboxes the run
with bwrap in "degraded mode" — using only whichever --unshare-xxx namespaces the
running kernel actually supports, instead of requiring the full set.
Status
Early-stage, but functional. Mounting, running, dropping privileges to a specific
user/group, and running in the background (-D/--daemonize) all work, as do
-x/--exec-ing into and --kill-ing a running session. Named volumes (-v/--volume)
and named networks (-n/--network, with a veth or tap+relay join, IPv4 NAT for
--extern networks, and per-session DNS resolution) can be created and persist in
the config file. A small docker-compose.yaml subset is also supported directly
(-u/--up/-d/--down, see below) for starting/stopping a handful of interdependent
containers together.
Requirements
Build-time:
- Meson + a C++20 compiler
fmt,libarchive,nlohmann_json,yaml-0.1(libyaml)spdlog(uses the system package if found, otherwise fetched automatically via the vendoredsubprojects/spdlog.wrap)catch2, only if theenable_testsMeson option is on (default: on)scdoc, optional — builds theslocker-lite(1)man page fromdocs/slocker-lite.1.scd; skipped with a message if not found, never a hard requirement
Runtime:
containers-storagefuse-overlayfsbwrap(bubblewrap)nsenter(only needed for non-root runs, and for-n/--network, which is root-only regardless — see "How it works" below)ip,iptables,sysctl(only needed for-n/--network;iptables/sysctlonly for--externnetworks, root-only)dnsmasq(optional; enables per-session DNS resolution of other containers'--hostnames on a shared network — degrades to a warning and no DNS if missing, or skip it deliberately with--no-dns)
Build
meson setup buildDir
meson compile -C buildDir
meson test -C buildDir
This also builds buildDir/slocker-lite-priv-drop, a small statically-linked helper
that -r --user/--group needs at runtime (see "How it works").
Testing
meson test -C buildDir above runs three checks: the original fixture mount/unmount
smoke test, plus two of the four test categories -t/--test provides (see below) --
meson test only ever runs what's safe unprivileged with no network setup; the other
two categories are for a developer to run by hand on a real machine.
-t/--test runs this project's own built-in Catch2
test suite directly: slocker-lite -t [-- <catch-command-line-options>]. A bare -t
runs everything Catch2 finds; select a category with a tag expression (note the --
before it, needed since Catch2's own -r/--reporter would otherwise collide with
slocker-lite's own -r/--run):
| Category | What it needs | Invocation |
|---|---|---|
| 1. Unit | Nothing | slocker-lite -t -- "[unit]" |
| 2. Integration | Nothing (no network, no root) | slocker-lite -t -- "[integration]~[net]" |
| 3. Integration, networked | A real image (see below), no root | slocker-lite -t -- "[integration][net]~[root]" |
| 4. Integration, root | Root | slocker-lite -t -- "[integration][root]" |
Append ~[slow] to any of these to skip notably slow cases. A [root]-tagged test
SKIP()s cleanly (not a failure) when not actually run as root, and a
[net]-tagged one does the same when it needs a real container image it can't find
(see next) -- so a bare -t degrades gracefully rather than failing outright on a
plain rootless dev machine with no fixture set up.
Categories 3 and 4 need a real, runnable OCI image (something with an actual
/bin/sh, unlike the minimal single-file fixture the plain meson test smoke test
uses) at images/busybox.tar. Run tests/setup-tests.py to fetch one automatically
(via skopeo, podman, or docker, whichever is available -- does nothing if
images/busybox.tar already exists, e.g. your own build); or just drop your own
busybox-like image there directly.
Usage
slocker-lite -r|--run <image.tar> [-v <name-or-dir> <container-path>]... [-n <network>]... [-p [<network>:]<host-port>:<container-port>[/tcp|udp]]... [-- <command> [args...]]
slocker-lite -x|--exec <pid> [-- <command> [args...]]
slocker-lite --kill <pid>
slocker-lite -l|--list-images <directory>
slocker-lite -i|--inspect <image.tar>
slocker-lite -v|--volume <name> <directory>
slocker-lite --list-volumes
slocker-lite --delete-volume <name>
slocker-lite --delete-volume-full <name>
slocker-lite -n|--network <name> (--extern|--intern) [--subnet <cidr>] [--with-ipv6=<bool>] [--subnet6 <cidr>] [--with-veth=<bool>]
slocker-lite --list-networks
slocker-lite --delete-network <name>
slocker-lite --delete-network-full <name>
slocker-lite --list-processes
slocker-lite --clean-processes
slocker-lite [-c|--config-file <path>] -w|--write-config
slocker-lite -t|--test [-- <catch-command-line-options>]
slocker-lite -u|--up <images-dir> [compose-file]
slocker-lite -d|--down [compose-file]
slocker-lite --list-containers
slocker-lite -h|--help
slocker-lite -V|--version
# Debug-only (see the bottom of the flag table below):
slocker-lite --mount <image.tar>
slocker-lite --umount <layer-id>
slocker-lite --cleanup <layer-id>
| Flag | Description |
|---|---|
-r, --run <image.tar> |
Mount, run bwrap in the foreground, then unmount and clean up on exit. Defaults to the image's own Entrypoint/Cmd (or /bin/sh if neither is set); pass -- <command> [args...] to override. |
--no-nsenter |
With --run, bind the mount directly instead of nsenter-ing into fuse-overlayfs's namespace. Automatic when running as root; use this to force it off otherwise. |
-D, --daemonize |
With --run, fork into the background: detaches from the controlling terminal (setsid()), ignores SIGHUP, and redirects stdin from /dev/null and stdout/stderr to a log file under $XDG_STATE_HOME/slocker-lite/logs/. Prints the session's pid and log path, then returns — the same pid --list-processes/-x/--exec use. |
--user <user> |
With --run, run the command as this user (name or numeric uid) instead of the image's own declared user (or root, if it declares none). Resolved against the image's own /etc/passwd. Only takes effect when --run executes as root. |
--group <group> |
With --user, use this group (name or numeric gid) instead of the user's primary group. |
--hostname <name> |
With --run, set the sandbox's hostname. Only takes effect if the running kernel supports --unshare-uts; ignored with a warning otherwise. |
--env VAR=VALUE |
With --run, set an environment variable in the sandbox (overrides the default PATH/HOME/PWD/TERM if given the same name). Repeatable; combined with --env-file in command-line order, each later one winning over an earlier one for the same name. |
--env-file <file> |
With --run, load environment variables from <file> — one VAR=VALUE per line; blank lines and #-comments are skipped. Repeatable. |
-l, --list-images <dir> |
List OCI Image Layout tars (*.tar, *.tar.*) found directly in <dir>, with their name:tag. |
-i, --inspect <image.tar> |
Print an image's declared user, exposed ports, env, volumes, and default command, without mounting or running it. |
-x, --exec <pid> |
Join an already-running --run session (<pid> must be one --list-processes shows as running) and run a command inside its container. Pass -- <command> [args...] to specify it. |
--kill <pid> |
Stop a running --run session (<pid> must be one --list-processes shows as running): sends SIGTERM, waits up to 10s, then forces it with SIGKILL. Reaches every process the session started — including daemonized/reparented ones a plain kill <pid> would leave behind — via a dedicated cgroup when available, or the sandboxed pid namespace's own collapse-on-kill guarantee when not, falling back to signaling the tracked pid alone if neither applies. |
-v, --volume <name> <dir> |
Create a named volume mapped to a host directory (created if missing), recorded in the config file's volumes section. Fails if the name or directory is already used by an existing volume. Volume names can't contain /. With --run, instead mounts a volume into the sandbox (repeatable): <name> is an existing named volume, or, if it contains /, a host directory path (created if missing); <dir> is the absolute path inside the container to mount it at. If the host directory is empty and the image already has content there, that content is copied in first, preserving numeric ownership/permissions/links and, where the host filesystem supports them, extended attributes/ACLs (skipped with a warning otherwise). |
--list-volumes |
List all named volumes (see -v/--volume) with their host directory. |
--delete-volume <name> |
Remove a named volume from the config. The host directory is left untouched. |
--delete-volume-full <name> |
Like --delete-volume, but also recursively deletes the volume's host directory. |
-n, --network <name> |
Create/manage a persistent named network: requires exactly one of --extern (a bridge in its own dedicated namespace, with an uplink out to the host's real network so containers reach it via IPv4 NAT/forwarding — IPv6, if enabled, is same-bridge reachability only, no NAT: its ULA addresses are non-globally-routable by design, and this kernel's IPv6 stack has no NAT support at all regardless) or --intern (a bridge inside its own dedicated, routeless namespace, only reachable by other containers on the same network). --subnet <cidr> overrides the auto-allocated IPv4 range (10.168.0.0/24, incrementing per network); --with-ipv6=false disables (and --subnet6 <cidr> overrides) the auto-allocated IPv6 range. --with-veth=false forces the tap+relay join fallback even on a kernel that supports veth (useful for testing that path; it's otherwise chosen automatically whenever the running kernel lacks veth support). Both default to the config file's own global.with-ipv6/global.with-veth (enabled if unset either way — see -w/--write-config below) when not given explicitly. With --run, instead joins <name> to the container as its own eth<N> interface with an address from the network's subnet; repeatable, no membership limit. Root-only for now. See docs/networking-design.md for the full design, including the tap+relay veth fallback. |
--list-networks |
List all named networks (see -n/--network) with their kind, IPv4 subnet, bridge name, and IPv6 subnet (or (no ipv6)). |
--delete-network <name> |
Remove a named network from the config. Its live bridge/namespace/iptables state is left untouched. |
--delete-network-full <name> |
Like --delete-network, but also tears down its live state (the IPv4 MASQUERADE rule and the bridge itself for extern, or the whole persistent namespace for intern), so a network with the same name can be created fresh afterward. |
-p, --port-forward [<network>:]<host-port>:<container-port>[/tcp|udp] |
With --run, forward a port from the host into the container -- TCP by default, or UDP with an explicit /udp suffix. <network> is optional, defaulting to the container's sole --extern network (an error if it joined more than one without specifying). Repeatable, including the same port pair once per protocol. Reachable via the host's real, externally-facing IP; localhost/loopback access has a known NAT-hairpinning limitation, for both protocols (see docs/networking-design.md). |
--no-dns |
With --run, skip starting the per-session dnsmasq resolver even if it's available (see "How it works" below). Has no effect if no -n/--network was given, or if dnsmasq isn't installed. |
--list-processes |
List running --run sessions found by their pid files under $XDG_STATE_HOME/slocker-lite/run/, with their pid, container name, and status (running or exited). |
--clean-processes |
Remove stale pid files (see --list-processes), and any -p/--port-forward iptables rules or -n/--network tap-relay/DNS-resolver processes, left behind by sessions that are no longer running (e.g. after a crash). |
-c, --config-file <path> |
Use <path> instead of the default config.yaml for the global section only (log-level, unshare-*, with-veth, with-ipv6) — volumes/networks always come from the separate, always-fixed persistent.yaml (see Configuration below), never affected by this. Errors if <path> doesn't exist (no silent fall-back to defaults, unlike the default path). Combine with -w to bootstrap a config file at a custom location. Always available, regardless of command. |
-w, --write-config |
Write a complete global config file (creating it, and its parent directory, if missing), filling in every global option's current or default value. Useful to bootstrap one for hand-editing. Never touches volumes/networks. Prints the file's full path. |
-t, --test [-- <catch-command-line-options>] |
Run the built-in Catch2 test suite (see Testing above). |
-u, --up <images-dir> [compose-file] |
Start a docker-compose.yaml-subset project (see "Compose support" below): mount every service's image, provision its networks/volumes, then start each service's container in dependency order, daemonized. <images-dir> is where the referenced image tars live; compose-file defaults to compose.yaml in the current directory. Root-only if the compose file declares any networks. |
-d, --down [compose-file] |
Stop every container a matching -u/--up run started (found via a state file recorded at -u time) and tear down any networks it created that are no longer in use by anything else; named volumes are always left in place. compose-file defaults the same way as -u/--up. |
--list-containers |
List containers started by -u/--up, with their compose file, service name, container name, pid, and status (running or exited). |
--log-level <level> |
Set log verbosity (trace, debug, info, warn, error, critical, off). |
-h, --help |
Print usage and exit. |
-V, --version |
Print version information and exit. |
Earlier versions used -m/-u/-c as short forms for --mount/--umount/--cleanup.
Those letters have since been reassigned — -u to --up and -c to
--config-file — since compose orchestration and per-invocation config overrides
are both far more commonly used than these three debug commands. --mount/
--umount/--cleanup are still available, long-option-only; see the bottom of
this table.
| Flag | Description |
|---|---|
--mount <image.tar> |
Validate and mount an OCI Image Layout tar. Debug-only — see -r/--run/-x/--exec for the normal way to actually use an image. |
--umount <layer-id> |
Unmount a previously mounted layer (the ID printed by --mount/--run, or from containers-storage layers). Debug-only — -r/--run already unmounts on its own when done. |
--cleanup <layer-id> |
Delete a layer and its ancestor chain from local storage (unmount it first). Debug-only, same reason as --umount above. |
Examples
# Mount an image and inspect it (prints the merged mount path) -- debug-only,
# see the note above the flag table
./buildDir/slocker-lite --mount myimage.tar
# Mount, run the image's default command, then unmount and clean up
./buildDir/slocker-lite -r myimage.tar
# Run a specific command instead
./buildDir/slocker-lite -r myimage.tar -- /bin/sh -c 'echo hello'
# Run as a specific user (as root only)
sudo ./buildDir/slocker-lite -r myimage.tar --user git
# Run with a custom hostname inside the sandbox
./buildDir/slocker-lite -r myimage.tar --hostname mybox
# Run with extra environment variables, from flags and/or a file
./buildDir/slocker-lite -r myimage.tar --env FOO=bar --env-file ./app.env
# Run in the background; prints its pid and log file, then returns
./buildDir/slocker-lite -r myimage.tar -D
# List every OCI image tar in a directory
./buildDir/slocker-lite -l ./images
# Inspect an image's declared config without mounting or running it
./buildDir/slocker-lite -i myimage.tar
# Start a busybox container in the background, then get a shell inside it from
# another terminal (find its pid with --list-processes)
./buildDir/slocker-lite -r busybox.tar &
./buildDir/slocker-lite --list-processes
./buildDir/slocker-lite -x 12345 -- /bin/sh
# Create a named volume backed by a host directory
./buildDir/slocker-lite -v mydata ~/slocker-volumes/mydata
# Run, mounting that named volume plus an ad hoc host directory
./buildDir/slocker-lite -r myimage.tar -v mydata /data -v ~/scratch /scratch
# List all named volumes
./buildDir/slocker-lite --list-volumes
# Remove a named volume (keeps its host directory)
./buildDir/slocker-lite --delete-volume mydata
# Remove a named volume and delete its host directory too
./buildDir/slocker-lite --delete-volume-full mydata
# Create a network with outside connectivity, then run a container joined to
# it, forwarding a host port into it (root only)
sudo ./buildDir/slocker-lite -n mynet --extern
sudo ./buildDir/slocker-lite -r myimage.tar -n mynet -p 8080:80
# Start every service in a compose.yaml-subset project in the background,
# mounting its images from ./images (see "Compose support" below)
sudo ./buildDir/slocker-lite -u ./images
sudo ./buildDir/slocker-lite -u ./images my-other-compose.yaml
# List containers started by -u/--up
./buildDir/slocker-lite --list-containers
# Stop everything a matching -u/--up run started (volumes are kept; networks
# are torn down only if nothing else is still using them)
sudo ./buildDir/slocker-lite -d
sudo ./buildDir/slocker-lite -d my-other-compose.yaml
# List currently running (and any leftover, exited) --run sessions
./buildDir/slocker-lite --list-processes
# Remove any leftover, stale pid files
./buildDir/slocker-lite --clean-processes
# Bootstrap a config file at a custom location (global section only --
# volumes/networks are unaffected, see Configuration below)
./buildDir/slocker-lite -c ~/alt-slocker.yaml -w
# Use that alternate file's global settings for one run
./buildDir/slocker-lite -c ~/alt-slocker.yaml -r myimage.tar
Configuration
Persistent settings are split across two local YAML files, both under
$XDG_CONFIG_HOME/slocker-lite/ (falling back to
$HOME/.config/slocker-lite/ if XDG_CONFIG_HOME isn't set):
config.yaml— theglobalsection only (log verbosity, bwrap namespace policy, network-creation defaults). This is the file-c/--config-filecan point elsewhere for a single invocation.persistent.yaml— thevolumesandnetworkssections: real, provisioned host state (named volumes, bridges/namespaces). Always this one fixed file —-c/--config-filenever affects it, so an experiment with an alternate global config can never corrupt or shadow real volumes/networks.
# config.yaml
global:
log-level: debug
unshare-user: on
unshare-ipc: on
unshare-pid: on
unshare-net: on
unshare-uts: on
unshare-cgroup: on
with-veth: on
with-ipv6: on
# persistent.yaml
volumes:
mydata: /home/user/slocker-volumes/mydata
networks:
mynet:
kind: extern
subnet: 10.168.0.0/24
ipv6: true
subnet6: fdf0:f243:f06f:168::/64
veth: true
global.log-level sets the default log verbosity (an explicit --log-level on
the command line always overrides it, whether the config came from the default
config.yaml or a -c override). The six global.unshare-<type> keys
control whether -r/--run requests the matching bwrap --unshare-xxx flag
(only namespace types the running kernel actually supports are ever affected
either way) — each accepts 1/on/yes/true or 0/off/no/false,
case-insensitively, and defaults to on (enabled) when unset, so the block
above is also the default with nothing configured. Note on unshare-net:
leaving it enabled (the default) means a plain -r/--run with no -n/--network
gets an isolated net namespace with nothing but loopback — there's no
automatic, unprivileged slirp4netns-style default network yet, so real
connectivity currently requires root and an explicit -n/--network join (see
above). Set unshare-net: off instead if you'd rather the sandbox just see the
host's own network directly. global.with-veth/global.with-ipv6 (same accepted values,
same "on unless set otherwise" default) give -n/--network's own creation-time
--with-veth/--with-ipv6 a persistent default, so e.g. a dev machine that's
always used to exercise the tap+relay fallback can set with-veth: off once
instead of passing --with-veth=false on every network creation — an explicit
--with-veth/--with-ipv6 on the command line still overrides it for that one
call. No other long options belong in config.yaml (one-shot
commands like --mount/--run/--user don't).
persistent.yaml's volumes section is managed by -v/--volume (see above)
rather than hand-edited — it's what -r/--run's own -v usage looks named
volumes up in. Its networks section is likewise managed by -n/--network
rather than hand-edited — see docs/networking-design.md
for the full persistent-network feature design. -n/--network both creates a
network (standing up its real bridge/iptables state, root-only) and, combined
with -r/--run, joins a container to one or more of them with its own
interface and address on each (a real veth pair, or a tap+relay fallback where
veth isn't available); -p/--port-forward then forwards a host port
into a container on one of its --extern networks. Missing config files are
fine either way (nothing is overridden, and each file gets created the first
time something needs to write to it).
-c/--config-file <path> uses <path> instead of the default
config.yaml for the global section only, for that one invocation —
persistent.yaml is always read from its one fixed location regardless, and
if <path> itself happens to contain volumes/networks sections (e.g. an
old-format file reused by mistake), those are simply ignored, never read or
migrated. Unlike the default config.yaml (missing = defaults, not an
error), a -c-given path that doesn't exist is a hard error, since pointing
at a specific file is a deliberate choice.
Migrating from an older single-file config.yaml: versions before this
split kept volumes/networks directly in config.yaml alongside global.
The first time a newer build runs against such a file, it automatically
moves any volumes/networks it finds there into persistent.yaml and
rewrites config.yaml down to just its global section — logged at info
level (migrated N volume(s) and M network(s) from ... to ...). This only
ever inspects the default config.yaml, never a -c-given file. A name
collision against an already-existing persistent.yaml entry aborts the
migration for that run (neither file is touched) with a warning identifying
the conflict, rather than silently dropping or overwriting anything.
Run -w/--write-config to bootstrap a config.yaml (or, combined with -c,
an alternate global file at a custom location): it writes out every
supported global option explicitly (filling in the current or default
value for anything not already set — so the first block above is exactly
what a fresh -w produces), creating the file and its parent directory if
they don't exist yet, and prints the file's full path. It never touches
persistent.yaml. Combine it with other flags to seed specific values, e.g.
slocker-lite --log-level debug -w writes log-level: debug.
Compose support
-u/--up/-d/--down run a small subset of a docker-compose.yaml directly,
without docker/podman/docker-compose installed. Supported per-service keys:
image, container_name, user ("user[:group]", same default-to-the-image's-
own-user behavior as -r/--run), command (list or shell-string form),
environment/env_file (same precedence as --env/--env-file: later wins),
depends_on (list or mapping form; services start in dependency order),
stop_grace_period, networks, ports (-p's own syntax), and volumes
(SRC:DST[:MODE] short form only). Top-level networks: entries are either
internal: true/false (mapped to a managed intern/extern slocker-lite
network, created automatically) or external: true (must already exist, created
by hand ahead of time with -n/--network). Top-level volumes: entries become
named slocker-lite volumes, auto-created on first use. Anything else in the file
(build, deploy, healthcheck, restart, ...) is silently ignored. See
test-compose/compose.yaml for a worked example
with two interdependent services, env_file, port forwarding, and all three
network kinds.
-u/--up <images-dir> [compose-file] resolves each service's image: against
the OCI tars in <images-dir> (matched by name:tag, same as -l/--list-images),
mounts every image first (so a slow mount on one service doesn't delay starting
the others), provisions the declared networks/volumes, then starts each
service's container in dependency order, daemonized (as if -D were given to
each). A state file recorded under $XDG_STATE_HOME/slocker-lite/compose/ maps
the compose file back to the pids -d/--down/--list-containers need.
-d/--down [compose-file] stops every one of those containers and tears down
any network -u created that's no longer in use by anything else (external
networks are never touched); named volumes always persist across -d/-u
cycles, matching real Compose's own default.
How it works
Image layers are imported into containers-storage (parent-chained) and the
resulting top layer is mounted via fuse-overlayfs; -r/--run then sandboxes the
requested command with bwrap. Because containers-storage mount runs rootless by
re-execing into a private user+mount namespace, -r/--run normally has to nsenter
into that namespace to reach the mount — except when running as root, where the mount
is already directly visible and --unshare-user is skipped entirely (a fresh user
namespace isn't needed for root's own privilege, and forces an unrelated
supplementary-group bug in that case). Running as root also unlocks --user/
--group: since bwrap --uid/--gid require a user namespace that isn't available
there, slocker-lite instead bind-mounts a separate, statically-linked helper
(slocker-lite-priv-drop) into the sandbox and routes the command through it to drop
privileges before exec.
While a -r/--run session is active, its bwrap process is tracked as a locked
PID file under $XDG_STATE_HOME/slocker-lite/run/ (falling back to
$HOME/.local/state/...), named after the image and its PID so the same image can
be run concurrently without collisions. The file is removed automatically once the
run ends; any tool can check whether a session is still alive by attempting the
same exclusive, non-blocking flock() on its file. --list-processes does
exactly that for every pid file it finds, reporting each one's pid, container
name, and running/exited status. Normally the file is removed automatically
when its own session ends, but --clean-processes removes any stale ones left
behind (e.g. after a crash) using that same check, atomically per file, so it
never removes one that's still genuinely running.
-x/--exec <pid> joins a running session's namespaces with nsenter and runs a
command there. Because bwrap itself sets up the sandbox's mount/user namespaces
and then hands the actual sandboxed command off to a child process in fresh
pid/uts/ipc/cgroup namespaces, -x resolves that real child first (via /proc)
rather than joining the outer bwrap process's own namespaces, so the joined
command sees the container's process tree and hostname too, not just its
filesystem.
--kill <pid> stops a running session and everything it started — not just
the tracked bwrap process. A plain kill <pid> can leave processes behind: a
container whose entrypoint daemonizes a service (double-forks and detaches)
before exec-ing its main command can end up with that service reparented
somewhere bwrap dying never reaches, especially on a kernel without pid
namespace support, where it reparents all the way to the host's own pid 1.
--kill picks between three mechanisms depending on what's actually
available for that session: a dedicated cgroup (set up at -r/--run time,
reliably includes every process the session ever started regardless of
daemonizing or pid namespace support — the most complete option, when the
kernel and permissions allow it), the sandboxed pid namespace's own
collapse-on-kill guarantee (when --unshare-pid was genuinely in effect for
that session), or, failing both, signaling the tracked process directly (no
worse than today's manual kill). Either way it sends SIGTERM first,
waits up to 10 seconds, then escalates to SIGKILL.
-D/--daemonize forks and detaches into the background by calling setsid()
itself, rather than re-enabling bwrap's own --new-session — that flag only
detaches the deeply-nested sandboxed command, leaving bwrap/nsenter still
attached to the original session. Calling setsid() in slocker-lite's own
forked child, before it execs into nsenter/bwrap, detaches the whole chain
at once (exec() never changes session membership), and correctly scopes
bwrap's own --die-with-parent to that child. The child ignores SIGHUP and
redirects output to a log file before doing anything else; the original,
still-foreground process waits only long enough to learn the real session pid
(the same one --list-processes/-x/--exec use) before printing it and
returning — the detached child is what runs the entire session afterward,
including the same unmount/cleanup that always ran once the sandboxed command
exits.
-n/--network creates a bridge in its own dedicated, persistent network namespace
(root-only) — an --extern one additionally gets a point-to-point uplink out to
the host's real network, giving joined containers IPv4 NAT connectivity (IPv6 is
same-bridge reachability only, by design — see
docs/networking-design.md). With -r/--run, each
-n <name> joins the container to that bridge as its own eth<N>, normally via a
real veth pair; on a kernel without CONFIG_VETH (or with --with-veth=false at
creation time), a userspace tap+relay pair stands in for it instead, producing the
same end state. If dnsmasq is installed, each session that joins any network
also gets a private per-session resolver, so a container can resolve any other
container's --hostname on a shared network (and host.containers.internal, for
an --extern network's own gateway) — degrades to a warning and no DNS if
dnsmasq is missing, or can be skipped deliberately with --no-dns.
-u/--up/-d/--down don't introduce a second, separate execution model: each
service they start is just an ordinary -r/--run, daemonized the same way -D
already is, in its own forked process — one OS process per running container,
whether started directly or via a compose file. -u/--up records the pids it
started (keyed by the compose file's path) to a small state file so -d/--down
and --list-containers can find them again later; -d/--down stops each one the
same way --kill does and, only for a managed (non-external) network, checks
whether anything else is still using its bridge (any other still-attached
interface) before tearing it down, so networks shared between compose projects
(or with manually-run containers) are left alone.
See CLAUDE.md for the full architecture writeup (file-by-file breakdown, the
reasoning behind each of the above, and known gaps).
License
GPL-2.0-or-later. See COPYING.