# Testcontainers against Werkdock Werkdock serves the Docker Engine API subset Testcontainers uses, so a test that asks for a real PostgreSQL gets one — without docker, without podman, without root. Validated with Testcontainers 1.19.3 and 1.21.4 and `postgres:16-alpine`, on a developer machine and on a Hostsharing managed webspace (RFC 0005 milestones 3 and 4), both programmatically and through `jdbc:tc` URLs. ## Setting it up ```bash werkdock pull postgres:16-alpine # once; needs the crane binary werkdock system service & # listens on $XDG_RUNTIME_DIR/werkdock.sock export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/werkdock.sock export TESTCONTAINERS_RYUK_DISABLED=true ``` That is the whole adaptation: one socket and one environment variable. Ryuk, Testcontainers' cleanup container, wants the API socket bind-mounted into a container of its own and is therefore not supported; `--rm`, `werkdock rm`, and Testcontainers' own session cleanup cover the same ground. A test then reads as it always does: ```java try (PostgreSQLContainer pg = new PostgreSQLContainer<>("postgres:16-alpine")) { pg.start(); try (Connection c = DriverManager.getConnection(pg.getJdbcUrl(), pg.getUsername(), pg.getPassword())) { // pg.getMappedPort(5432) is where the server really listens } } ``` ## Or no container code at all: `jdbc:tc` URLs Testcontainers can be the JDBC driver itself, which is how most projects use it — the database is then a line of configuration: ```yaml spring: datasource: driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver url: jdbc:tc:postgresql:16-alpine:///mydb?TC_DAEMON=true&TC_INITSCRIPT=init.sql ``` This works against werkdock with the same two environment variables and asks for no endpoint the programmatic form does not. Two flags are worth knowing: - `TC_DAEMON=true` keeps the container while the JVM lives. Without it Testcontainers stops the database when the last connection closes, and a connection pool closes connections whenever it likes. - `TC_INITSCRIPT` names a script on the classpath, which Testcontainers runs **over JDBC**. No file is copied into the container, so it needs nothing werkdock lacks. [Werkprobe](https://git.javagil.de/mi/werkprobe) exercises both forms against werkdock in its test suite. To keep the socket available without starting it by hand, a systemd user unit does it (the managed webspaces have a user manager and lingering enabled): ```ini [Service] ExecStart=%h/opt/werkdock/bin/werkdock system service Environment=WERKDOCK_HOME=%h/.werkdock ``` `werkdock system service --time 30m` exits when nothing has used it for that long, podman's manner; without `--time` it runs until stopped. ## What is different from docker, and why **Ports are moved, not mapped.** The sandbox shares the host's network (RFC 0002), so there is nothing to map a port with. When a client asks for a published port, werkdock picks a free one and tells the *service* to bind it, through the environment variable the image honours, then reports that port to the client. `getMappedPort(5432)` therefore returns a real, free port and the JDBC URL works — but the server inside is genuinely listening on that port, not on 5432. The images werkdock knows how to move: | Image | Variable | |---|---| | `postgres` | `PGPORT` | | `mariadb` | `MARIADB_TCP_PORT` | | `mysql` | `MYSQL_TCP_PORT` | | `redis` | `REDIS_PORT` | Any other image keeps its own port, which is correct as long as one instance of it runs at a time. Asking to publish such a port somewhere else fails with the reason rather than reporting a port nothing listens on. Adding an image is one line in `quirks` (package `api`). **One user.** An unprivileged user namespace maps the caller to exactly one uid, so an entrypoint that starts as root to drop privileges later (the official `postgres` image and its gosu dance) is started as its target user right away; those entrypoints detect a non-root uid and skip the dance. The same table holds that knowledge. **Refused rather than ignored.** A parameter that would change the answer is not quietly dropped: `logs?tail=10`, `timestamps=true`, and a `since`/`until` other than zero all answer 501 with the reason, because werkdock streams the recorded output whole and records no timestamps beside it. **Everything else answers 501 with the reason.** Werkdock implements the traffic a real session sends ([docs/testcontainers-api.md](testcontainers-api.md)), not the whole Engine API. A client that needs more gets a plain explanation, never a 404, and the service log names the endpoint. Known gaps, none of which `PostgreSQLContainer` needs: `PUT /containers/{id}/archive` (`withCopyFileToContainer` and anything else that puts a file inside a container — a JDBC init script does not, it travels over the connection), networks, and `docker build`. ## Inside a build sandbox The intended use is an integration test *inside* a werkdock build, which reaches the service by having the socket bound in: ```bash werkdock run --rm -v /repo:/repo -w /repo \ -v $XDG_RUNTIME_DIR/werkdock.sock:/run/werkdock.sock \ -e DOCKER_HOST=unix:///run/werkdock.sock \ -e TESTCONTAINERS_RYUK_DISABLED=true \ buildenv sh -c './gradlew test' ``` The database then runs as a sibling of the build sandbox, not inside it, which is exactly how docker-in-docker is done with a mounted socket.