werkator build succeeded after 00:03
Three of its four entries were wrong. redis, mariadb and mysql were all listed as moving their port through an environment variable — REDIS_PORT, MARIADB_TCP_PORT, MYSQL_TCP_PORT. None of the three images reads one. Werkdock picked a free port, told nobody, and reported it: a client got a number nothing was listening on, which is the single failure the whole quirk mechanism exists to prevent. It was found the only way it could be — by starting the images and connecting to the port werkdock named. redis-server takes --port; the two database entrypoints read the port out of the server's own configuration, so the flag has to reach the command line there too. A quirk can now move a port by a flag, and does so only while the container still runs the image's own program: appending an argument to a command the client chose would be a guess, and guessing is what this commit is about. Two more things surfaced on the way. Redis could not start at all — its entrypoint chowns its data directory as root, which an unprivileged user namespace cannot do, so it needs the same treatment postgres has. And an image's /run did not show through: /run is the instance's own directory so that sockets survive a stop, which left mariadb without /run/mysqld and dying on "Bind on unix socket: No such file or directory". The instance's /run is now seeded from the image's. postgres was right all along and is untouched; it really does read PGPORT. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
364 lines
18 KiB
Markdown
364 lines
18 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 a branch
|
|
that changes behavior 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".
|
|
|
|
## 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.
|