652 lines
34 KiB
Markdown
652 lines
34 KiB
Markdown
# Changelog
|
|
|
|
Release notes for consumers of the `werkdock` CLI, Werkator first among them.
|
|
Each entry says what changed, what still works unchanged, and what a consumer
|
|
should adapt — and by when.
|
|
|
|
A release is a merge to main; there is no separate release step. So each
|
|
branch sets the version in `internal/cli/cli.go` (what `werkdock version`
|
|
prints) and titles its section here with that version and the merge date,
|
|
before it is merged. Nothing on main is ever "unreleased". A branch that
|
|
changes nothing a consumer notices gets a section marked as internal.
|
|
|
|
## 0.13.3 (2026-10-20)
|
|
|
|
Theme: bwrap starts with an empty environment. Since werkator PR#166
|
|
werkdock's own environment holds the values of a build's variables,
|
|
with names a branch may choose; bwrap, a host process, inherited them.
|
|
It needs none: its arguments and fd 4 carry everything.
|
|
|
|
### For consumers
|
|
|
|
- Nothing changes in the CLI, the API or what the sandbox sees.
|
|
|
|
## 0.13.2 (2026-10-20)
|
|
|
|
Theme: secrets off the command line. werkdock passed each variable
|
|
of the sandbox as `bwrap --setenv KEY VALUE`, so every `ps` showed the
|
|
secrets of a build. bwrap now reads them from a pipe (`--args 4`);
|
|
its command line names none. And each instance, also of `run --rm`,
|
|
wrote its environment to `instances/<id>/config.json` with mode 0644;
|
|
an instance directory is now 0700, its files and logs 0600.
|
|
|
|
### For consumers
|
|
|
|
- Nothing changes in the CLI, the API or what the sandbox sees.
|
|
- A value with a NUL byte is refused; no environment can hold one.
|
|
- Another account no longer reads an instance's files; a tool that did
|
|
must run as the account that owns the store.
|
|
- Pass a secret as a bare `-e KEY` with the value in werkdock's own
|
|
environment, as Werkator does since werkator PR#166; a `-e KEY=VALUE`
|
|
stands in werkdock's own command line.
|
|
|
|
## 0.13.1 (2026-10-20)
|
|
|
|
Theme: the Go that builds werkdock. `go.mod` names the toolchain
|
|
go1.26.10. With `GOTOOLCHAIN=auto`, the default, an older Go loads it
|
|
and builds with it: the CI's build image has Go 1.24.4.
|
|
Until now the Go of the machine built the binary, and with it the
|
|
standard library: govulncheck found 7 vulnerabilities that werkdock calls
|
|
in go1.26.8, among them in `net/http`, which serves the API socket, and
|
|
more in older Go. The binary on mih09 came from go1.22.2.
|
|
|
|
### For consumers
|
|
|
|
- Nothing changes in the CLI or the API. Install the new binary on
|
|
mih09; `go version -m werkdock` names go1.26.10.
|
|
- Building from source with an older Go loads go1.26.10 by default
|
|
(`GOTOOLCHAIN=auto`) and needs the network for it once; with
|
|
`GOTOOLCHAIN=local` the older Go builds, as before.
|
|
|
|
## 0.13.0 (2026-10-20)
|
|
|
|
Theme: the verb that was "not planned".
|
|
`werkdock build` builds an image from a Dockerfile on the target itself — no second machine, no upload.
|
|
RFC 0004 had ruled it out because it would need "writable rootfs snapshots per step and `pull` for FROM";
|
|
`pull` arrived in 0.3.0, and per-step snapshots turned out to be what a build *cache* needs, not what a build needs.
|
|
[RFC 0006](docs/rfcs/0006-dockerfile-builds.md) has the measurements.
|
|
|
|
### Added
|
|
|
|
- **`werkdock build [-f Dockerfile] -t IMAGE CONTEXT`** (docker's flags).
|
|
`FROM` resolves through the store and pulls on a miss, one writable work tree is created over the base,
|
|
the steps run against it, and the result is an ordinary flat werkdock image — `images`, `inspect`, `rmi`,
|
|
`run` and `--mount type=image` apply to it unchanged.
|
|
Staging is the mechanism `import` has used since 0.11.0: build in `<image>.tmp`, hold that image's `flock`,
|
|
`rename` at the end, so a failed build leaves no half image and a build racing an `import` of the same reference waits for it.
|
|
- **`FROM scratch`** starts from an empty tree, as docker's reserved name does; nothing is looked up or pulled.
|
|
- **Stage 1 of the instruction set:** `FROM`, `RUN`, `COPY`, plus the metadata instructions `import -c` already knew —
|
|
`ENTRYPOINT`, `CMD`, `ENV`, `WORKDIR`, `USER`, `VOLUME`, `LABEL` — through the same `store.ApplyChange`,
|
|
so a Dockerfile line and a `--change` line are parsed by one piece of code.
|
|
Line continuations with `\`, comments and blank lines are handled; `RUN` takes the shell and the exec form.
|
|
- **Both uid levers are werkdock's business, not the Dockerfile's**, so the same Dockerfile still builds under docker.
|
|
Measured on a Debian trixie rootfs with one mapped uid:
|
|
plain `apt-get update` dies with `seteuid 42 failed` and `Method http has died unexpectedly`,
|
|
so werkdock writes `APT::Sandbox::User "root"` into the work tree for the duration of the build and removes it at commit.
|
|
`apt-get install nginx` then still fails — `chown: changing ownership of '/var/log/nginx': Invalid argument`, dpkg exit 1 —
|
|
and `fakeroot` alone does *not* fix it: its `chown` wrapper calls the real syscall and reports its `EINVAL`.
|
|
`fakeroot` plus `FAKEROOTDONTTRYCHOWN=1` does, so `RUN` steps are wrapped in `fakeroot` when the base image has one,
|
|
and `build` says so up front when it has not.
|
|
- **Two backends behind one interface, of which one is implemented.**
|
|
The copy backend (a real full copy of the base, root bound read-write) works on the bwrap 0.8 the webspaces run today.
|
|
The overlay backend is designed, not written:
|
|
`--overlay-src` stacks unprivileged on bwrap 0.11 and its upper directory is already a layer diff — measured,
|
|
including deletions appearing as character devices 0:0, which is what OCI writes as `.wh.NAME`.
|
|
It is what the per-step cache waits for; without a union mount a cached step would cost a whole tree copy.
|
|
The copy backend stays as the fallback.
|
|
- **`doctor` probes the writable root bind** and names the backend `build` will use, so the two never disagree.
|
|
|
|
### Changed
|
|
|
|
- **`engine.RunSpec` has `WritableRoot`**: the rootfs is bound with `--bind` instead of `--ro-bind`.
|
|
Only build steps set it.
|
|
A sandbox with a writable root also gets **no implicit tmpfs** over `/tmp`, `/root` and `/run` — it does not need one,
|
|
and with one, what a `RUN` step writes there would be gone in the next step while docker keeps it in the image.
|
|
Runs are unchanged: read-only root, tmpfs as before.
|
|
- `werkdock image build` works as `docker image build` does, next to the top-level verb.
|
|
|
|
### Refused rather than ignored
|
|
|
|
- `EXPOSE`, `HEALTHCHECK`, `STOPSIGNAL`, `ONBUILD` and `MAINTAINER` fail at parse time with the reason,
|
|
before a 2 GiB tree is copied.
|
|
`ARG`, `SHELL`, `ADD` and multi-stage builds (`FROM … AS`, `COPY --from`) name the stage of RFC 0006 that brings them.
|
|
`COPY --chown`/`--chmod` is refused: the sandbox maps one uid, and ownership is dropped when an image is unpacked anyway.
|
|
On the flag side `--build-arg`, `--target`, `--platform`, `--secret`, `--ssh`, `--cache-from`, `--cache-to`, `--squash`,
|
|
`--network`, `--output`, `--load`, `--push` and `--progress` each fail with what they would need.
|
|
`--no-cache` is accepted and reported as the no-op it is.
|
|
- `mknod` and `mount` inside a `RUN` step cannot be made to work: `CapEff` is `0000000000000000` in the sandbox, measured,
|
|
so every mount has to stand in bwrap's argv.
|
|
BuildKit's `RUN --mount=type=cache` is out for that reason.
|
|
|
|
### Known limitation
|
|
|
|
- **No build cache.**
|
|
Every build starts at `FROM` and re-runs every step, network `RUN` steps included.
|
|
This is the copy backend's one real cost and the reason the overlay backend is worth writing after the Debian 13 upgrade.
|
|
A tree copy itself is cheap: 0.31 s for 129 MiB over 5448 inodes on local ext4,
|
|
and 6.8 s for a whole build over a 143 MB base with an `apt-get install nginx jq` in it.
|
|
For a ~2 GiB build image on shared webspace storage, budget tens of seconds — that is an extrapolation, not a measurement.
|
|
Peak disk is base plus work tree until the commit's `rename`.
|
|
- One `-t` per build: a second reference for one image needs a `tag` verb, which werkdock does not have yet.
|
|
|
|
### For consumers
|
|
|
|
- Werkator can build its build environment on the target:
|
|
`werkdock build -t werkator-buildenv:trixie .` in place of `docker build` elsewhere plus `docker save`,
|
|
upload and `werkdock load`.
|
|
The base image needs `fakeroot` installed (one `apt-get install fakeroot`), or every package whose postinst chowns will fail;
|
|
`build` reports the omission before it starts.
|
|
Nothing existing changes: `import`, `load`, `pull`, `run` and the API behave as in 0.11.0,
|
|
and an image built this way is indistinguishable from a loaded one.
|
|
|
|
## 0.12.1 (2026-10-20)
|
|
|
|
Internal: werkdock follows the shared agentic workflow of the Werk
|
|
repositories, with reviews by werkrevisor, and its build checks the
|
|
licenses and the called vulnerabilities of its dependencies. The CLI is
|
|
unchanged.
|
|
|
|
## 0.11.0 (2026-10-05)
|
|
|
|
Theme: two imports at once, and a command that cannot start. Both seen on
|
|
mih09, where Werkator 1.37.0 started a CI build and a pull-request review
|
|
of one repository together, and both imported the same new rootfs.
|
|
|
|
### Fixed
|
|
|
|
- **Imports of one image wait for each other**, across processes. Import
|
|
stages in `<image>.tmp` and renames it into place, and it removes that
|
|
directory first, so the next import cleans up after an interrupted one.
|
|
A second import running at the same time removed the first one's files
|
|
under it: one failed with `latest.tmp/image.json: no such file or
|
|
directory`, the other's sandbox could not execute `/bin/sh`. An import
|
|
now holds an exclusive `flock` on `locks/import-<hash>.lock` below
|
|
`$WERKDOCK_HOME`; the kernel drops it when the holder dies. The import
|
|
that waited finds the image and refuses with `already exists`, exactly
|
|
as an import after it would.
|
|
|
|
### Changed
|
|
|
|
- **`werkdock run` answers 127 or 126 like docker** when the command
|
|
cannot be started: 127 when no executable of that name is in the
|
|
image (`missing: executable file not found in $PATH`), 126 when one is
|
|
there but cannot be executed (`/bin/sh: permission denied`). Until now
|
|
bwrap's own exit code 1 came back, which a caller cannot tell from the
|
|
command's own failure. The lookup follows execvp: the name itself when
|
|
it holds a slash, else every `PATH` directory in order, symlinks
|
|
resolved inside the rootfs. A path under a mount, the implicit tmpfs
|
|
areas included, is not checked, since its content is not known before
|
|
the sandbox starts. Nothing is created for a refused command.
|
|
- `create` and `start`, and so the Docker API, are unchanged: docker's
|
|
create does not check the command either.
|
|
|
|
### For consumers
|
|
|
|
- Werkator needs no change: since 1.37.2 it imports one image at a time
|
|
per instance itself, and the lock here also covers a second instance
|
|
or a shell. An exit code 127 or 126 of `werkdock run` can still come
|
|
from the command — `sh -c` answers 127 for a missing tool — so it is
|
|
no proof that the sandbox failed; the message on stderr says which.
|
|
|
|
## 0.10.0 (2026-09-11)
|
|
|
|
Theme: the port that lives in a file. Mosquitto, listed in 0.9.0 as the
|
|
image werkdock cannot move, can be moved after all — not by anything
|
|
mosquitto offers, but by the one thing werkdock can do that a flag or an
|
|
environment variable cannot: hand the server a different config file.
|
|
|
|
### New
|
|
|
|
- **A quirk can name a config file.** `PortFile` says which file the
|
|
image ships names the port and how the line reads (`listener 1883`).
|
|
Werkdock copies that file into the instance directory with the line
|
|
rewritten to the chosen port and binds the copy read-only over the
|
|
image's original, which is never touched. The mount is the last one
|
|
in `inspect`, at the file's own path. It applies only while the
|
|
command still names the file, and not when the client binds a file of
|
|
their own over that path — then the copy would not be what the server
|
|
reads, and the port is reported unmoved instead.
|
|
- **Mosquitto** (`eclipse-mosquitto`) moves this way: both listeners,
|
|
MQTT on 1883 and the dashboard's HTTP API on 9883, out of
|
|
`/mosquitto/config/mosquitto.conf`. Measured with 2.1.2 through the
|
|
API socket: two instances at once, each on its own ports, each
|
|
accepting an anonymous client and answering on its dashboard port,
|
|
with 1883 left free. What was tried and does not work, so nobody
|
|
tries again: `-p` is ignored once a config file declares a listener
|
|
(the manual says so; measured), no environment variable names the
|
|
port, and the config file expands none.
|
|
|
|
### Refused rather than ignored
|
|
|
|
- An image whose config file no longer has the line werkdock expects is
|
|
refused at create with the line and the file named, rather than
|
|
started on a port werkdock only claimed. A quirk is measured knowledge
|
|
about one image version; a changed image is a reason to measure again,
|
|
not to guess.
|
|
|
|
### Known limitation
|
|
|
|
- A file copied in through `PUT /containers/{id}/archive` after create
|
|
(Testcontainers' `withCopyFileToContainer`) lands over the rewritten
|
|
copy, because copies bind after everything else. A client that
|
|
supplies mosquitto's config that way gets the ports that config says,
|
|
not the ones werkdock reported. Supply it as a bind (`withFileSystemBind`)
|
|
instead, which werkdock sees at create and steps aside for.
|
|
|
|
## 0.9.0 (2026-09-11)
|
|
|
|
Theme: messaging. NATS, RabbitMQ and Mosquitto join the quirk table, each
|
|
measured against the running image rather than its documentation — and each
|
|
taught the table something it could not express before.
|
|
|
|
### New
|
|
|
|
- **NATS** (`nats`): moved with `--port` on `nats-server`, which wins over
|
|
the port in the config file the image ships. Nothing else needed.
|
|
- **RabbitMQ** (`rabbitmq`): moved with `RABBITMQ_NODE_PORT`, runs as
|
|
`rabbitmq` because its entrypoint chowns as root. It also needs a **cap**
|
|
on werkdock's choice, which is new: the Erlang distribution port is the
|
|
node port plus 20000, so anything above 45535 refuses to boot with
|
|
"Invalid Erlang distribution TCP port". The kernel's ephemeral range
|
|
starts at 32768, so more than half of werkdock's previous choices would
|
|
have been wrong. A quirk can now say `MaxHostPort`, and werkdock looks
|
|
for a free port itself when there is one.
|
|
- **Mosquitto** (`eclipse-mosquitto`): runs as `mosquitto` — it drops
|
|
privileges itself and cannot setgroups in a user namespace — and is
|
|
listed with **no port knob at all**. Its port comes from the config the
|
|
image ships, whose explicit `listener` switches off the default one that
|
|
`-p` would set; measured, the flag is ignored. So werkdock reports 1883
|
|
unmoved, which means one instance at a time, the same rule that holds for
|
|
every image not in the table. An entry that only names a user is a
|
|
legitimate entry.
|
|
|
|
### Refused rather than ignored
|
|
|
|
- A host port above an image's cap answers 400 with the reason, rather than
|
|
starting a server that dies in an Erlang stack trace.
|
|
|
|
### Unchanged
|
|
|
|
- Every CLI verb, every endpoint, the instance directory format, and the
|
|
four images that were already in the table.
|
|
|
|
## 0.8.1 (2026-09-11)
|
|
|
|
Theme: a copy that stops one level too early. `MariaDBContainer` could not
|
|
start; anything that copies into a directory the image fills could not
|
|
either.
|
|
|
|
### Fixed
|
|
|
|
- **A copied tree is merged at whatever depth the two first differ**, not
|
|
refused at the top. 0.7.0 decided once, at the top level of the archive:
|
|
a directory the image filled was refused. Testcontainers copies
|
|
mariadb's configuration to `/etc/mysql/conf.d`, and docker's clients put
|
|
the whole path in the tar, so the top level was `etc` — refused, and the
|
|
container never started. The rule now descends: bind where the image has
|
|
nothing, descend where it has a directory, bind the file where it has a
|
|
file. Copying into `/etc/mysql/conf.d` now binds one file four levels
|
|
down and leaves `/etc` alone.
|
|
|
|
With it goes the 501 that said werkdock "would have to hide them": there
|
|
is no longer a case where it would.
|
|
|
|
### Unchanged
|
|
|
|
- Everything else about the endpoint, including what it still refuses: a
|
|
copy into a running instance, symlinks and device nodes, entries that
|
|
climb out of the destination, and archives above 64 entries.
|
|
|
|
## 0.8.0 (2026-09-11)
|
|
|
|
Theme: the quirk table, measured instead of assumed. Three of its four
|
|
entries were wrong, and each would have handed a client a port number that
|
|
nothing was listening on. A consumer running `redis`, `mariadb`, or `mysql`
|
|
through the API should take this release.
|
|
|
|
### Fixed
|
|
|
|
- **`redis`, `mariadb`, and `mysql` were published on the wrong port.**
|
|
All three were listed as moving their port through an environment
|
|
variable — `REDIS_PORT`, `MARIADB_TCP_PORT`, `MYSQL_TCP_PORT`. None of
|
|
the three images reads one: redis-server takes `--port`, and the two
|
|
database entrypoints read the port out of the server's own
|
|
configuration. Werkdock chose a free port, told nobody, and reported it.
|
|
Measured against the real images, not guessed: all three now move by a
|
|
command-line flag and were verified to answer on the port they report.
|
|
- **`redis` could not start at all.** Its entrypoint chowns its data
|
|
directory as root, which an unprivileged user namespace cannot do; it
|
|
now runs as `redis`, the same treatment `postgres` already had.
|
|
- **An image's `/run` was empty.** `/run` is the instance's own directory
|
|
so that sockets survive a stop, which meant nothing of the image's
|
|
showed through — and `mariadb` died with "Bind on unix socket: No such
|
|
file or directory" because `/run/mysqld` was missing. The instance's
|
|
`/run` is now seeded from the image's: directories, and files small
|
|
enough to be a word rather than data.
|
|
|
|
### New
|
|
|
|
- A quirk may move a port by a **command-line flag**, not only by an
|
|
environment variable, and does so only while the container still runs
|
|
the image's own program — werkdock does not append a flag to a command
|
|
a client chose. Where it cannot, the port is reported unmoved, as
|
|
before.
|
|
|
|
### Unchanged
|
|
|
|
- `postgres` was right and is untouched: it really does read `PGPORT`.
|
|
- Every CLI verb, every endpoint, and the instance directory format.
|
|
|
|
## 0.7.0 (2026-09-11)
|
|
|
|
Theme: a file into a container — `PUT /containers/{id}/archive`, the last
|
|
endpoint Testcontainers asks for that werkdock answered with a 501. Purely
|
|
additive; nothing that worked before behaves differently.
|
|
|
|
### New
|
|
|
|
- `PUT /containers/{id}/archive` unpacks a tar into an instance, which is
|
|
what `withCopyFileToContainer` and `MountableFile` send. An init script
|
|
copied to `/docker-entrypoint-initdb.d` is now run by the image's own
|
|
entrypoint, as on docker.
|
|
- The copy does **not** go into the image. A rootfs in the store is shared
|
|
by every instance of that image and bound read-only, so the tar is
|
|
unpacked into the instance directory and bound in — whole, where the
|
|
image has nothing at the destination, and file by file where it does, so
|
|
that the image's own files stay visible beside the copied ones. That is
|
|
docker's merge, reached by another road.
|
|
|
|
### Refused rather than ignored
|
|
|
|
- Copying into a **running** instance answers 501 with the reason: a
|
|
sandbox's mounts are fixed when it starts, and werkdock has no writable
|
|
container layer to put the file in. Copy before start, as
|
|
`withCopyFileToContainer` does, or write through a volume.
|
|
- Copying a **directory** onto one the image fills answers 501: the bind
|
|
would hide what is inside, and werkdock will not do that quietly.
|
|
- A symlink, hard link, device node, or fifo in the archive answers 400 —
|
|
each would have to mean something inside the sandbox that werkdock
|
|
cannot promise — as does an entry that climbs out of its destination.
|
|
- An archive of more than 64 entries answers 400: every entry may become a
|
|
bind, and a client that wants to populate a tree wants an image.
|
|
|
|
### Unchanged
|
|
|
|
- Every CLI verb, every other endpoint, and the instance directory format
|
|
(`config.json` stays version 1; a copy adds mounts to the spec it
|
|
already holds).
|
|
|
|
## 0.6.0 (2026-09-10)
|
|
|
|
Theme: the API socket — RFC 0005 milestone 3, and with it milestone 4:
|
|
Testcontainers starts a real PostgreSQL in werkdock. Purely additive;
|
|
no CLI verb changes behaviour.
|
|
|
|
### New
|
|
|
|
- `werkdock system service [--time D] [unix://PATH]` serves the Docker
|
|
Engine API subset a client actually uses, on
|
|
`$XDG_RUNTIME_DIR/werkdock.sock` by default (podman's verb and
|
|
podman's manners: optional, started when needed, `--time` to exit
|
|
when idle). A client is pointed at it with
|
|
`DOCKER_HOST=unix://<path>`.
|
|
- Testcontainers (validated with 1.19.3, `PostgreSQLContainer`) works
|
|
against it: pull, create, start, logs with the wait strategy, exec,
|
|
kill, remove, and the session cleanup. It needs
|
|
`TESTCONTAINERS_RYUK_DISABLED=true`, because Ryuk wants the API
|
|
socket inside a container of its own.
|
|
- The endpoints are those measured from a real run, written down in
|
|
`docs/testcontainers-api.md`. Everything outside the subset answers
|
|
**501 with the reason**, never 404, so a client that needs more says
|
|
so plainly.
|
|
- Instances carry docker labels (`config.json`), and the instance list
|
|
filters on them, which is how a client finds what it left behind.
|
|
- An exec is a record of its own with an id and an exit code, because
|
|
the API creates, starts, and inspects it in three calls.
|
|
|
|
### Refused rather than ignored
|
|
|
|
- `logs` parameters werkdock cannot honour say so with 501 and the
|
|
reason: `tail` (the output is streamed whole), `timestamps` and a
|
|
non-zero `since`/`until` (no clock is recorded beside the output).
|
|
What a client actually sends — `stdout`, `stderr`, `follow`,
|
|
`since=0`, `tail=all` — is served.
|
|
|
|
### Ports, the deliberate deviation
|
|
|
|
Werkdock shares the host's network and cannot map a port. Where a
|
|
client asks for a published port, werkdock therefore picks a free one
|
|
and tells the *service* to bind it, through the environment variable
|
|
the image honours (`PGPORT`, `MARIADB_TCP_PORT`, `MYSQL_TCP_PORT`,
|
|
`REDIS_PORT`). An image outside that small table keeps its own port,
|
|
which is right as long as one instance of it runs at a time; asking to
|
|
move it fails with the reason instead of silently reporting a port
|
|
nothing listens on. The same table carries the user an image needs when
|
|
its entrypoint expects to drop privileges itself (`postgres`), which
|
|
werkdock cannot do with one uid.
|
|
|
|
### Version reporting
|
|
|
|
`/version` and `/info` report the Docker Engine version whose API is
|
|
served (25.0.0), not werkdock's: clients compare that field against a
|
|
minimum, and 0.x fails it forever. Werkdock's own version is beside it
|
|
in `Components` and in `/info`'s `OperatingSystem`.
|
|
|
|
### For Werkator
|
|
|
|
- Nothing to change. The service is not started unless someone asks
|
|
for it, and no CLI verb behaves differently.
|
|
- New possibility: an integration test in a build sandbox can get a
|
|
real database. Bind the socket into the sandbox
|
|
(`-v $XDG_RUNTIME_DIR/werkdock.sock:/run/docker.sock`) and set
|
|
`DOCKER_HOST` and `TESTCONTAINERS_RYUK_DISABLED` in the build.
|
|
|
|
## 0.5.0 (2026-09-10)
|
|
|
|
Theme: the instance lifecycle — RFC 0005 milestone 2. A service image
|
|
runs in the background, can be stopped, restarted, queried, and talked
|
|
to. Additive for `run --rm`; two small semantic changes below.
|
|
|
|
### New
|
|
|
|
- `run -d` starts the instance under a background supervisor and prints
|
|
its 64-hex id; `--name NAME` names it (default: image basename plus six
|
|
characters of the id); `-i` passes stdin (no pty: `-t` is refused).
|
|
- `ps [-a] [-q] [--format FMT]` lists instances with docker's columns
|
|
(`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES`; PORTS stays
|
|
empty, the network is the host's); `stop [-t SECONDS]`, `kill [-s SIG]`,
|
|
`rm [-f]`, `wait`, `logs [-f]`, `create`, `start`, and `inspect` on an
|
|
instance id, unique prefix, or name. Each prints what docker prints
|
|
(the argument on success, `werkdock: …` and exit 125 per failure).
|
|
- `exec [-e K=V] [-w DIR] [-u USER] [-i] INSTANCE CMD...` runs a second
|
|
command in the instance's sandbox: same rootfs, mounts, uid, env,
|
|
`/run` and volumes; not the same pid namespace.
|
|
- docker's management form is accepted: `container ls|start|stop|...`,
|
|
`image ls|rm|pull|...`.
|
|
- State lives in `$WERKDOCK_HOME/instances/<id>/`: `config.json`
|
|
(`version: 1`), `state.json`, `stdout`, `stderr`, `run/`, `volumes/`.
|
|
A state left "running" by a dead supervisor (reboot, `kill -9`) is
|
|
repaired to exited 255 on the next read; an exited `--rm` instance
|
|
found that way is removed.
|
|
|
|
### Changed
|
|
|
|
- A `run` without `--rm` no longer fails ("persistent instances are not
|
|
implemented yet"); the instance stays for `ps -a`, `logs`, `start`, `rm`.
|
|
- The command is pid 1 of its sandbox (`bwrap --as-pid-1`), as in
|
|
docker, so `stop`'s SIGTERM can reach it. As in docker, an entrypoint
|
|
without a SIGTERM handler (`sh -c '…'`) is only ended by the SIGKILL
|
|
after the timeout, and orphaned children are its own to reap.
|
|
- A foreground `run` forwards the first SIGINT/SIGTERM/SIGHUP to the
|
|
command and kills it on the second, or after 10 seconds; bwrap runs in its own process
|
|
group, so Ctrl-C no longer kills bwrap (and with it the command)
|
|
outright. Werkator's builds see the same exit codes as before; a
|
|
`sh -c` build interrupted with SIGTERM now ends 10 seconds later, on
|
|
the SIGKILL, unless the script traps TERM.
|
|
- Anonymous volumes (the image's `VOLUME`s) and `/run` are directories
|
|
of the instance instead of tmpfs, so they survive `stop`/`start` and
|
|
are visible to `exec`. For a `--rm` run the difference is disk instead
|
|
of RAM for those paths, and nothing else.
|
|
|
|
### For Werkator
|
|
|
|
- Nothing to change: `run --rm …` behaves as before. The `ps` output
|
|
is new and Werkator does not parse it.
|
|
|
|
## 0.4.0 (2026-09-10)
|
|
|
|
Theme: service images — the first milestone of RFC 0005 (PostgreSQL in the
|
|
sandbox, Testcontainers as the destination). Additive; a `run` without the
|
|
new flags on an image without `USER`/`VOLUME` behaves exactly as before.
|
|
|
|
### New
|
|
|
|
- `run --user USER[:GROUP]` (`-u`) makes the caller appear as that user inside
|
|
the sandbox: a name is resolved in the image's `/etc/passwd` and `/etc/group`,
|
|
a number needs no entry. `HOME` follows the passwd entry when there is one.
|
|
Still one uid only — the caller is mapped to it; there is no second user.
|
|
- `run --userns=keep-id` (podman's spelling) maps the caller to their own uid
|
|
and gid. Other `--userns` modes are refused with the reason.
|
|
- An image's `USER` (OCI config) applies when neither flag is given, as with
|
|
podman; unknown names fail with `image USER: …`. Default stays uid 0.
|
|
- An image's `VOLUME` paths are mounted as anonymous tmpfs unless a `-v` or
|
|
`--mount` covers them, so `postgres:16` starts without knowing its data and
|
|
socket directories by heart.
|
|
- `/run` is a tmpfs in every sandbox, beside `/tmp` and `/root`. `/var/run`
|
|
points there in practically every image, and services keep pid files, locks
|
|
and sockets under it without declaring a `VOLUME` (`postgres` needs
|
|
`/var/run/postgresql` writable). A `--tmpfs`/`-v` on a path below `/run`
|
|
still works, it just lands inside the tmpfs.
|
|
- `import -c` accepts `USER user[:group]` and `VOLUME /path` (or a JSON array);
|
|
`inspect` shows `User` and `Volumes` in `Config`.
|
|
|
|
### For Werkator
|
|
|
|
- Nothing to change. Builds keep running as uid 0. If a build image ever sets
|
|
`USER`, that user applies; add `--user 0` to keep root.
|
|
|
|
### Podman before Docker
|
|
|
|
- Where the two differ, werkdock follows podman from now on (RFC 0005):
|
|
`--userns=keep-id` is the first case.
|
|
|
|
## 0.3.0 (2026-09-10)
|
|
|
|
Theme: `werkdock pull` — images come straight from a registry, no docker
|
|
on any machine needed (RFC 0002 level 2, RFC 0004). Additive; nothing a
|
|
consumer calls today changes.
|
|
|
|
### New
|
|
|
|
- `werkdock pull [--platform OS/ARCH] [REGISTRY/]REPOSITORY[:TAG]` fetches an
|
|
image and stores it exactly as `load -i` would: layers flattened, OCI
|
|
config kept, listed by `images` under the reference given. Prints
|
|
`Pulled image: REF`; exit 125 with the reason if the registry refuses,
|
|
the image already exists, or crane is missing. `--platform` defaults to
|
|
`linux/<host arch>`.
|
|
- The registry side is the `crane` binary
|
|
(github.com/google/go-containerregistry, one static executable, no
|
|
privileges). werkdock looks for it in `PATH`, then beside its own
|
|
binary, or at `WERKDOCK_CRANE`. A deployment on a managed webspace ships
|
|
it next to `werkdock`. Credentials come from `~/.docker/config.json`
|
|
(`DOCKER_CONFIG`), as with docker.
|
|
- `werkdock rmi IMAGE...` removes images (`docker rmi`): prints `Untagged: REF`
|
|
per image, tries every argument and exits 125 if one failed. Works on the
|
|
legacy flat layout too, so Werkator's pre-0.2.0 build image can be removed
|
|
with it. `pull` and `load` now name `rmi` in their "already exists" message.
|
|
- `werkdock images` takes docker's `--format`: a Go template over
|
|
`.Repository`, `.Tag`, `.ID`, `.CreatedAt`, `.CreatedSince`, `.Size`, or
|
|
`table [TEMPLATE]` for docker's column layout (`--format table` prints
|
|
`REPOSITORY TAG IMAGE ID CREATED SIZE`); `-q` prints IDs; an optional
|
|
`REPOSITORY[:TAG]` argument filters. Without flags the output is unchanged:
|
|
one short reference per line. `inspect` gains an `Id` field. The ID is
|
|
a hash of the image metadata, shared by tags of the same image; it is
|
|
not docker's config digest.
|
|
- `werkdock doctor` warns when crane is not in `PATH` (a warning, not a
|
|
failure: everything but `pull` works without it).
|
|
|
|
### For Werkator
|
|
|
|
- Optional: replace "download archive, `werkdock load -i`" with a single
|
|
`werkdock pull IMAGE` where the image is a public registry image; the
|
|
self-built archives keep going through `import`/`load`.
|
|
- Deployment: put `crane` beside `werkdock` if `pull` is to be used.
|
|
- Optional: retire old build images with `werkdock rmi` instead of deleting
|
|
store directories.
|
|
|
|
## 0.2.0 (2026-09-10)
|
|
|
|
Theme: the `run` surface, image names, and the image store move closer to
|
|
`docker run --rm` (RFC 0002, RFC 0004). Everything Werkator invokes today keeps working; two of its calls
|
|
now have a preferred docker-shaped form.
|
|
|
|
### Compatible — nothing to change
|
|
|
|
- `werkdock images` still prints one image per line and still prints a bare
|
|
name for images loaded without a tag. Werkator's exact-line existence check
|
|
(`image in loaded`) matches as before. Images that carry a tag other than
|
|
`latest` print as `repository:tag`.
|
|
- `werkdock load -i ARCHIVE --name NAME` still imports a rootfs archive; it
|
|
now additionally prints one note on stderr pointing to `werkdock import`.
|
|
Exit code and stdout are unchanged (`Loaded image: NAME` became
|
|
`Imported image: NAME` — adjust if the stdout line is parsed).
|
|
- `werkdock run --rm -v ... --tmpfs ... -e ... -w ... IMAGE CMD...` is
|
|
unchanged in syntax and mount order. Two semantic corrections match docker:
|
|
a bare `-e KEY` whose variable the host does not have is no longer passed as
|
|
`KEY=` (it is omitted); a missing `-w DIR` is created when it lies in a
|
|
writable area (tmpfs or a read-write bind) instead of failing with bwrap's
|
|
"Can't chdir".
|
|
- Existing stores: the flat layout `images/NAME/` written by earlier versions
|
|
is still read as `NAME:latest`. No reload after the upgrade; new imports land
|
|
in `images/REPOSITORY/TAG/`. The pre-existing `image.json` files lack the new
|
|
metadata fields and read back as an image without ENTRYPOINT/CMD/Env, which
|
|
is exactly what they were.
|
|
- `werkdock doctor`, `version`, exit codes (125 for werkdock's own errors,
|
|
the command's code otherwise): unchanged.
|
|
|
|
### Preferred new forms — adapt at the next convenient change
|
|
|
|
- `werkdock import ARCHIVE [REPOSITORY[:TAG]]` replaces `load -i --name` for
|
|
rootfs archives (`docker import` semantics). Plan: `load -i --name` stays
|
|
accepted until Werkator has switched, then it is removed together with the
|
|
stderr note.
|
|
- `werkdock load -i ARCHIVE` loads a `docker save` archive (legacy and OCI
|
|
layout) or a plain OCI image layout: layers flattened into one rootfs,
|
|
whiteouts applied, the OCI config kept as image metadata, the image named by
|
|
its RepoTags (every tag becomes a hardlinked copy). A multi-arch index picks
|
|
the host platform. This is the intended path for build environments from now
|
|
on: `docker build`, `docker save`, upload, `werkdock load` — with ENTRYPOINT,
|
|
ENV, and WORKDIR intact (RFC 0004). Layers compressed with zstd need the
|
|
`zstd` binary, as `import` does.
|
|
- Image names may carry a tag: `werkator-buildenv-<hash>` keeps working and
|
|
equals `werkator-buildenv-<hash>:latest`; a consumer may also use
|
|
`werkator-buildenv:<hash>` or a registry-style `git.javagil.de/mi/buildenv:<hash>`.
|
|
Name grammar: `[registry/]repository[:tag]`, repository components lowercase
|
|
`[a-z0-9]` with `.`, `_`, `-` between them, tag `[A-Za-z0-9_][A-Za-z0-9_.-]*`,
|
|
max 128 characters. The `.tmp` tag suffix is reserved.
|
|
- `import -c 'ENTRYPOINT [...]'`, `-c 'CMD ...'`, `-c 'ENV K=V'`,
|
|
`-c 'WORKDIR /d'`, `-c 'LABEL k=v'` store OCI metadata with the image;
|
|
`werkdock inspect IMAGE` prints it as a docker-shaped JSON array
|
|
(`RepoTags`, `Created`, `Architecture`, `Os`, `Config`, plus a `Werkdock`
|
|
section with `Source` and `RootFS`). A consumer that wants to check
|
|
existence like `docker image inspect` can use `inspect` (exit 125 when the
|
|
image is missing) instead of scanning `images`.
|
|
- `werkdock run IMAGE` without a command runs the image's ENTRYPOINT+CMD;
|
|
`--entrypoint PROGRAM` overrides ENTRYPOINT and drops CMD; positional
|
|
arguments replace CMD; `-e` and `-w` override the image's Env and WorkingDir.
|
|
- `--mount type=bind,src=SRC,dst=DEST[,readonly]` and
|
|
`--mount type=tmpfs,dst=DEST` are accepted beside `-v` and `--tmpfs`; all
|
|
three feed one ordered mount list (the git-metadata mask ordering still
|
|
holds across them).
|
|
- `--network host` (and `--net host`) is accepted as a no-op, so a build
|
|
script can pass a docker-compatible command line unchanged. Other modes fail
|
|
with: `network mode "X" is not available in unprivileged shared-webspace
|
|
environments; only "host" is supported`. `-p/--publish` remain refused.
|
|
|
|
### Incompatible
|
|
|
|
- None for the calls Werkator makes. For everyone else: an image name with an
|
|
uppercase letter or a leading `.`/`-`/`_` in the repository was accepted by
|
|
the old name check and is refused now (docker's grammar); `..`-style names
|
|
were refused before and still are.
|