The two backends
native — the fast path
Runs ordinary host processes with worktree lifecycle, port reservations, per-worktree data and native process supervision. Zero virtualization overhead, no engine requirement, millisecond operations.
It does not isolate hardcoded loopback ports, absolute storage paths, Unix sockets, registries, shared credentials or arbitrary host access. Settings that accept a port or a data path work perfectly; settings that assume a fixed path or port do not.
container — one reusable container per workspace
Uses an existing local Docker or Podman engine. No VM, and no image is created per command: a prepared image supplies the toolchain, and the container is reused across up and down.
The image must provide bash, sh, sleep, socat, git, process-compose v1.122.0 and the project’s tools. The checked-in Dockerfile is a minimal example, not an attempt to bundle every language. Mutable image tags can change after a reset or recreation — use an image digest where reproducibility matters.
Execution context and addresses
The whole workspace — service graph, hooks, probes and one-off commands — executes in one runtime. There is no split where a hook runs on the host and a service runs in a container.
| Path or variable | Container mode |
|---|---|
| Working directory | /workspace, a writable bind mount carrying the checkout |
$BERTH_DATA_DIR |
/workspace/.berth/data |
| Shared Git metadata | Mounted at /berth/git; GIT_DIR and GIT_WORK_TREE point Git at the corresponding linked-worktree metadata |
| Home directory | The managed HOME is private to the container |
| Host credentials, Docker socket | Not mounted automatically |
Shells, argv and user mapping
Process definitions keep process-compose syntax. Native hooks use sh on Unix and cmd on Windows; container hooks use sh. berth does not make arbitrary shell strings portable between them.
berth run preserves argv exactly — it never reparses the command — while berth run -- sh -c … intentionally invokes a shell.
Rootless and user mapping depend on the engine: native Linux uses the host UID/GID inside Docker, rootless Podman uses keep-id, and rootless Docker may need an explicit runtime.user override matching its user namespace. Mounted filesystem permissions follow from that mapping.
Ports and publications
ports names the host publications a workspace needs; listen records the original TCP port the application uses in container mode. Every declared port requires a listen value. Internal services may also use undeclared ports, but berth does not discover or publish them dynamically.
| Variable | Who uses it |
|---|---|
BERTH_PORT_<NAME> |
Code inside the execution context: the service binds this value |
BERTH_HOST_PORT_<NAME> |
Code outside it — browser code, host tooling, a second process on the host |
Publications bind to host 127.0.0.1. Inside the container, an internal socat gateway forwards from a high container port to the application’s IPv4 loopback listener. plan.gateway_ports lists the reserved gateway ports, and application listeners must not bind them. Declaring the application’s listeners lets the allocator avoid them, including listeners in the 20000–39999 range.
Gateways are supervised by the container entrypoint: if a gateway exits, the container stops rather than silently leaving a publication broken.
The publication contract is TCP over IPv4 loopback — not UDP, and not IPv6-only loopback. Use a current engine: older Docker versions have different localhost-publishing security behaviour.
Network namespaces
Separate network namespaces prevent internal loopback and port collisions between workspaces. They are not egress firewalls and do not stop a process from reaching other host publications. Network access uses the engine’s normal bridge, NAT and DNS configuration. Unix sockets stored at workspace-relative locations stay distinct; sockets on shared host paths do not.
Readiness
berth up waits for declared health probes to report ready. A running process without a probe is accepted as started — which does not guarantee that its application endpoint is listening. Declare a readiness probe whenever a dependent command needs that guarantee.
Allow for cold startup in the probe’s initial delay and failure threshold: process-compose stops a process once its readiness failure threshold is reached (three failures by default). Native up also has a 60-second readiness deadline; raising the probe threshold does not remove that deadline or bypass readiness.
Read state from berth status --json (ready, healthy) rather than a sleep loop, and read failures from berth logs <proc>.
Lifetime, immutability and recovery
A stored runtime contract — backend, image reference, engine, limits and named ports — is immutable for an existing workspace. Changing processes, hooks or env inside the workspace is supported; changing the runtime or port contract requires a new workspace. That rule exists to avoid silently changing the meaning of a workspace while partial state is present, and to avoid controlling unrelated resources.
Container name and labels must match the stored workspace ID, canonical path and runtime specification before a stop or remove is allowed. A daemon failure is not treated as container absence. There is no implicit pull, no implicit build and no native fallback: when the engine or image is missing, the command fails instead of quietly running on the host.
Setup is idempotent by project responsibility: berth records completion and retries an incomplete setup, but projects must not assume arbitrary hook effects can be rolled back transactionally.
Stop, reset, done
berth downstops the workspace, preserving its checkout, data and container instance.berth resetdestroys the stopped runtime, resets private data, recreates it and reruns setup. Reset intent is persisted before the runtime or data changes, so if it fails or is interrupted the nextnew,uporrunfinishes the reset before retrying setup.berth donerecords the expected branch commit before removing the checkout, and never removes an adopted checkout. Any stop or identity failure blocks destructive work.
Cancelling a container-mode berth run stops that workspace’s whole container, so sibling services in the same workspace stop too; other workspaces keep running.
Interrupted removals
Retrying berth done can finish branch deletion and registration cleanup even after the checkout is gone. A replaced checkout, remaining Git worktree metadata, a checked-out branch or a changed branch commit blocks recovery, and --force does not override those checks. Branch deletion uses Git’s atomic old-value check. GC keeps these pending records and asks for an explicit done retry. Adopted workspaces retain their metadata as well as their data and branch.
What this is not
Neither backend is an adversarial security boundary. Shared Git metadata is writable, user hooks are code, and an explicitly supplied configuration may contain side effects. A sandbox for untrusted agents needs separate credentials, a filesystem policy, an egress policy and resource enforcement. Do not describe this runner as a complete security sandbox.
berth is a workspace manager: it decides which checkout, data directory, ports and processes belong to a task, and it makes their lifecycle safe to operate. Trust decisions about the code inside a workspace stay with you.