berth / Documentation / berth vs devcontainer / Docker

berth vs devcontainer / Docker

These two approaches draw the isolation boundary in different places. A devcontainer or a plain container isolates the machine; berth isolates the workspace, and only uses a container when the workspace genuinely needs Linux. This page states what each boundary buys, what it costs, and where berth is simply the wrong tool.

The honest summary comes first: if your requirement is operating-system isolation — a uniform Linux environment, a toolchain your host cannot provide, an environment you also run in CI, or code you do not fully trust — a devcontainer or a plain container is the right tool, and berth is not a replacement for it. Everything below explains why the two are still worth comparing, because berth has a container backend of its own and it does not behave like a devcontainer setup.

The axes

Design-axis comparison. Both columns describe design intent, not implementation: berth's statements come from its own documentation, and the container column describes the model rather than any particular tool.
Design axis berth devcontainer / Docker
Isolation boundary The workspace: linked worktree, private data directory, reserved ports, one process graph. Native mode shares the host kernel, filesystem and credentials The operating system: its own filesystem, users, network namespace and installed toolchain per project or session
Startup cost A worktree plus a short registry transaction; container mode reuses one prepared image per workspace and builds nothing per command Image build or pull, container create, mount setup, then start — repeated whenever the definition changes
Build and image model The image is an input, not a build step: one prepared Linux image per workspace, never built per command, never pulled implicitly, never silently replaced by running on the host The image is the environment: a definition you build, version and invalidate deliberately
Port allocation and publication Named ports reserved atomically with the workspace record; listen maps a declaration to the port the program actually binds; internal gateways forward to the application's IPv4 loopback listener; publications are TCP over IPv4 on host 127.0.0.1 A declared host mapping per service, unique per environment; keeping two environments from claiming the same host port is your job
Data isolation $BERTH_DATA_DIR per workspace: <workspace>/.berth/data natively, /workspace/.berth/data in a container. berth down keeps it; berth reset wipes it deliberately A container filesystem plus the volumes and bind mounts you declared; naming, lifetime and cleanup are part of the environment definition
Process supervision The declared process graph, supervised by process-compose natively or by the container engine in container mode, with readiness probes and status/logs The container lifecycle is supervised; supervision of the individual services inside it is something you describe or script
Host filesystem speed Native mode runs on the host filesystem. Container mode keeps the checkout on a writable bind mount, so it inherits the same platform penalty as any container Native speed on Linux; a bind-mount penalty on macOS and Windows, where the checkout crosses a filesystem boundary on every read and write
Toolchain and CI parity Whatever the host provides, plus a prepared Linux image when you choose the container backend and install your toolchain in it. berth does not claim that a local environment equals CI The strongest argument for this model: pin the versions once, and the same image can run locally and in CI
Security boundary Not an adversarial sandbox. Git metadata is shared and writable, hooks are user code, and there is no egress policy OS-level separation. A container with separate credentials, a filesystem policy and an egress policy is the stronger boundary

What the container model is genuinely good at

The case for an OS boundary is strong and berth does not argue with it. A container gives you one Linux userland regardless of the laptop underneath, which is the only practical answer when a toolchain officially ships for Linux and you develop on macOS or Windows. It pins language runtimes, system libraries and CLI versions in a file that reviewers can read. It keeps a long-lived project from polluting the host with language version managers, database servers and global packages. It gives you the best available parity story for CI, because the same definition can produce both environments. And it is the only one of the two approaches that keeps working when the code itself is not trusted: a container with its own credentials, filesystem policy, egress policy and resource limits is a boundary berth does not attempt to be.

What that boundary costs

The costs are equally real, and they land on the loop that agent-driven development runs fastest. Every environment pays boot work before the first command — image preparation, container creation and mount setup — and pays it again whenever the definition changes, which is the point at which you rebuild and re-pull everything that depends on the image. On macOS and Windows the checkout is typically bind-mounted from the host, so every file read and write crosses a filesystem boundary; on Linux the same setup runs at native speed. A whole guest userland is resident per environment rather than one process tree, which matters when several environments are open at once. And the model's own isolation gets in the way of the host toolchain: editors, language servers, debuggers and formatters need explicit wiring before they can see the same environment the build uses.

What berth does differently

berth isolates the workspace instead of the machine. The default backend is native: ordinary host processes, host filesystem, host toolchain, no virtualization overhead at all, with the workspace's ports, data directory and environment kept separate by construction rather than by a kernel boundary. That is the fast path, and it is why berth new does not need to boot anything.

The container backend exists for the two cases the native path cannot cover: a program that hardcodes a listen port, and a toolchain that only exists on Linux. It deliberately behaves nothing like a per-command container workflow. One prepared image is reused per workspace; nothing is built during a command; nothing is pulled implicitly; and if the engine or the image is missing, berth reports that and stops rather than quietly running the command on the host. The image must provide bash, sh, sleep, socat, git and process-compose v1.122.0 alongside your project tools, because the gateway that preserves hardcoded loopback ports is built from those pieces: the checkout is mounted at /workspace, shared Git metadata at /berth/git, and an internal forwarder carries traffic from a reserved high container port to the application's IPv4 loopback listener. plan.gateway_ports lists the internal reservations that application listeners must not use, and publications bind to 127.0.0.1 on the host.

version: 1
base: main
runtime:
  backend: container
  engine: docker
  image: berth-runtime:local
ports: [web]
listen:
  web: 8080          # the port the program hardcodes inside the container
berth new fixed-port --up      # native: host processes, no container
berth plan fixed-port            # the execution contract, before anything starts
berth ports                      # allocations and container listen mappings
berth status --json              # readiness, not sleep loops

Choosing a backend is a one-way door

Switch deliberately, because the decision is stored. The runtime and port contract — backend, image reference, engine, limits and the named port set — is immutable for an existing workspace, so moving from native to container (or the reverse) means creating a new workspace rather than editing the old one. That is a deliberate trade: it prevents a running environment from silently changing meaning or controlling resources it was not created for, at the cost of a migration when your needs change. The full rules, including what a probe sees in each mode, are in the runtime contract.

No implicit anything. berth never builds an image per command, never pulls one implicitly, and never falls back to running the command on the host when the engine or image is unavailable. A missing engine or a missing image is an error to fix, not a silent downgrade — the service runs on the host only when the configuration says native.

Security: what berth is not

berth is not an adversarial sandbox, and it should not be described as one. In native mode it isolates reserved ports, data directories and environment variables, and nothing else: hardcoded loopback ports, absolute storage paths, Unix sockets, registries, shared credentials and arbitrary host access are not isolated. Even in container mode, Git metadata is shared with the host and writable, hooks and process commands are user code that executes with the workspace's authority, and there is no egress firewall — a separate network namespace prevents internal loopback and port collisions, and does nothing to restrict outbound access. An untrusted agent needs separate credentials, a filesystem policy, an egress policy and resource enforcement. A container configured with those is the stronger boundary, and the correct choice for running code you do not trust.

If your requirement is OS-level isolation or CI parity, use the container. berth is not a replacement for a devcontainer or a plain Docker setup in that case, and its container backend is not an attempt to be one — it is one prepared Linux image per workspace, used to give the workspace a Linux userland, not to replace your environment definition.