76 lines
9.1 KiB
Markdown
76 lines
9.1 KiB
Markdown
# Werkdock
|
|
|
|
A docker-like sandbox CLI over `bwrap` — filesystem isolation only.
|
|
A dock is the enclosed basin in which ships are built: the dock gate controls what passes, the water outside is shared with the whole harbor.
|
|
Accordingly, network, uid, `/proc`, `/dev`, and `/tmp` come from the host by contract; that is what makes Werkdock work without root on a Hostsharing Managed Webspace.
|
|
|
|
Semantics — docker-compatible as far as the filesystem-only contract allows (see [RFC 0002](docs/rfcs/0002-docker-compatible-surface.md)):
|
|
|
|
- An *image* is a rootfs archive; an *instance* is an unpacked, writable directory tree and corresponds to a docker container.
|
|
- `import` takes a plain rootfs archive, as `docker import` does; `load` takes a `docker save` archive or an OCI image layout, flattens the layers (whiteouts honored, paths resolved inside the rootfs as in a chroot) and keeps the OCI config — so a Dockerfile-built image lands with its ENTRYPOINT, CMD, ENV, and WORKDIR ([RFC 0004](docs/rfcs/0004-docker-compatible-image-creation.md)). `load -i … --name` still routes a rootfs archive to `import`, for callers that predate it. `pull` fetches an image from a registry the same way, with the registry side (auth, manifest lists, blobs) delegated to the [crane](https://github.com/google/go-containerregistry) binary — a single unprivileged executable found in `PATH`, beside `werkdock`, or at `WERKDOCK_CRANE`; it reads `~/.docker/config.json` (`DOCKER_CONFIG`) for credentials as docker does. `images` lists one reference per line by default (what a script matches against) and takes docker's `--format`, with `--format table` giving docker's columns; `rmi IMAGE...` removes images.
|
|
- An image carries the execution-relevant OCI metadata (`Entrypoint`, `Cmd`, `Env`, `WorkingDir`, `User`, `Volumes`, `Labels`, architecture, os), set at `import -c 'ENTRYPOINT [...]'` the way `docker import --change` does; `inspect` prints it docker-shaped, and `run` without a command executes `ENTRYPOINT`+`CMD`.
|
|
- The caller is the sandbox's only user — an unprivileged user namespace maps exactly one uid. Which one is docker's choice: `--user USER[:GROUP]` (a name from the image's `/etc/passwd`, or a number), else the image's `USER`, else uid 0. Podman's `--userns=keep-id` maps the caller to their own uid. Service images that refuse root (PostgreSQL) run this way ([RFC 0005](docs/rfcs/0005-services-and-testcontainers.md)).
|
|
- The image's `VOLUME` paths get an anonymous volume unless a `-v`/`--mount` covers them: a directory of the instance, kept across `stop`/`start`, gone with `rm` — docker's anonymous volumes. `/run` (the target of `/var/run`) is an instance directory as well, so services keep pid files, locks, and sockets there without declaring it and `exec` finds the socket; `/tmp` and `/root` are tmpfs per run.
|
|
- Images are named the docker/OCI way, `[registry/]repository[:tag]` (`eclipse-temurin:21`, `ghcr.io/example/build:2026.09`), the tag defaulting to `latest`, so build configurations can use one reference for both.
|
|
- The network is the host's; `--network host` says so in docker's words and is accepted as a no-op, every other mode and `-p` are refused with the reason.
|
|
- Mounts are written as `-v SRC:DEST[:ro]`, `--tmpfs DEST`, or docker's explicit `--mount type=bind|tmpfs,...`; all three feed one ordered mount list.
|
|
- `werkdock run [flags] IMAGE [CMD...]` creates an instance and executes in the sandbox; verbs and flags follow docker, unsupported docker flags fail loudly.
|
|
- Instances have docker's lifecycle ([RFC 0005](docs/rfcs/0005-services-and-testcontainers.md) milestone 2): `run -d` starts a service under a small supervisor process and prints the id; `ps [-a]`, `logs [-f]`, `stop [-t]`, `kill`, `wait`, `rm [-f]`, `create`/`start`, `inspect` work on ids, unique prefixes, or `--name`s; `exec INSTANCE CMD` runs a second command in the same sandbox (same rootfs, mounts, uid, `/run`, volumes — not the same pid namespace). Instance state is one directory each under `instances/`: `config.json`, `state.json`, `stdout`, `stderr`, `run/`, `volumes/`. `--rm` foreground runs behave as before and record no logs. The command is pid 1 of its sandbox, as in docker: `stop` reaches it with SIGTERM if it handles the signal, SIGKILL follows after the timeout.
|
|
- `werkdock doctor` checks the host: user-namespace capability, disk and quota headroom.
|
|
- `werkdock system service` serves the Docker Engine API subset on a unix socket, so a client that speaks docker uses werkdock without knowing it. Testcontainers does: `PostgreSQLContainer` starts, waits, executes, and stops against werkdock ([RFC 0005](docs/rfcs/0005-services-and-testcontainers.md) milestones 3 and 4). The endpoints are the ones measured from a real run ([docs/testcontainers-api.md](docs/testcontainers-api.md)); everything else answers 501 with the reason. How to use it: [docs/testcontainers.md](docs/testcontainers.md).
|
|
- Ports are the one deliberate deviation: the sandbox shares the host's network, so werkdock cannot map a port. It picks a free one and tells the service to bind it through the image's own environment variable (`PGPORT` and its kin); an image outside that table keeps its port.
|
|
|
|
## Disk Footprint
|
|
|
|
Werkdock's storage model is coarser than Docker's on the image side and cheaper on the instance side:
|
|
|
|
- An image is a flat, complete directory tree — there are no layers, and nothing is shared between images.
|
|
A JDK+Go+Node build image is roughly 2 GiB unpacked, plus its compressed archive (~0.5 GiB) as long as that is kept around.
|
|
- An instance costs (almost) nothing: the rootfs is bound read-only into every sandbox, writable are only tmpfs (`/tmp`, `/root`), the instance's own `run/` and `volumes/` directories, and the caller's binds.
|
|
Ten parallel runs in one image add zero filesystem copies; what grows per project are its own caches in bound volumes, and per service its data volume until `rm`.
|
|
- Consequence: prefer ONE fat image shared by all projects over per-project images.
|
|
- Watch out for orphans: consumers that key an unpacked environment by the archive's source path (Werkator's bwrap runtime does) leave the old tree behind on every path change; pruning is manual until `rmi`/`prune` verbs exist.
|
|
- Future options that would remove the flat-tree cost, in their own RFCs when they come due: composable toolchain mounts — a slim base plus per-toolchain prefix binds, no overlayfs needed ([RFC 0003](docs/rfcs/0003-composable-toolchain-mounts.md), candidate; docker's `--mount type=image` is the compatible spelling, [RFC 0004](docs/rfcs/0004-docker-compatible-image-creation.md)) — overlayfs layers (the kernel allows it unprivileged in a user namespace since 5.11; the webspaces' bwrap 0.8.0 cannot yet), or hardlink deduplication between image versions in the store (the ostree principle, no root needed).
|
|
|
|
Changes that matter to consumers of the CLI (Werkator) are recorded in the [CHANGELOG](CHANGELOG.md), with what stays compatible and what to adapt.
|
|
|
|
## Build and Test
|
|
|
|
```bash
|
|
go test ./... # all tests; sandbox integration tests skip without bwrap/userns
|
|
go vet ./... && gofmt -l . # quality gates (gofmt must print nothing)
|
|
CGO_ENABLED=0 go build . # one static linux binary, ~3 MB
|
|
```
|
|
|
|
First steps on a host:
|
|
|
|
```bash
|
|
werkdock doctor # can this host run sandboxes?
|
|
werkdock import -c 'CMD ["bash"]' rootfs.tar.zst IMAGE # create an image from a rootfs archive (docker import semantics)
|
|
werkdock load -i image.tar # load a docker save / OCI archive (docker build … && docker save -o image.tar …)
|
|
werkdock pull eclipse-temurin:21 # fetch from a registry (needs the crane binary next to werkdock)
|
|
werkdock images --format table # REPOSITORY TAG IMAGE ID CREATED SIZE, as docker prints it
|
|
werkdock rmi eclipse-temurin:21 # remove an image (docker rmi)
|
|
werkdock inspect IMAGE # its metadata, docker-shaped JSON
|
|
werkdock run --rm -v /repo:/repo -w /repo IMAGE sh -c './gradlew build'
|
|
werkdock run -d --name pg --user postgres -e POSTGRES_PASSWORD=x -e PGPORT=54329 postgres:16-alpine # a service
|
|
werkdock ps; werkdock logs -f pg; werkdock exec pg psql -U postgres -c 'select 1'; werkdock stop pg; werkdock rm pg
|
|
```
|
|
|
|
For Testcontainers, run the socket and point a client at it:
|
|
|
|
```bash
|
|
werkdock system service & # $XDG_RUNTIME_DIR/werkdock.sock
|
|
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/werkdock.sock
|
|
export TESTCONTAINERS_RYUK_DISABLED=true
|
|
```
|
|
|
|
Status: bootstrap.
|
|
The implementation language is Go, decided in [RFC 0001](docs/rfcs/0001-implementation-language.md).
|
|
Werkdock grew in the `werkdock/` subdirectory of the [Werkator](https://github.com/mhoennig/werkator) repository and
|
|
moved here once it stood on its own (Werkator plan step 21, session E); the history above the split is preserved.
|
|
It stays self-contained: no imports from Werkator code, no coupling to the Werkator build — Werkator consumes the
|
|
binary through `PATH` (`bwrap.werkdock`), the way it consumes `git`.
|
|
Its own CI is the `.werkator.yml` in this repository; the roadmap that created it is session B of Werkator's
|
|
[plan step 21](https://github.com/mhoennig/werkator/blob/main/docs/plan/21-werkdock-extraction-and-webspace-install.md).
|