Give the image job a docker client to go with the daemon it already had
ci / android head (push) Failing after 5s
ci / build and test (push) Successful in 1m50s
ci / api image (push) Failing after 1m6s

Exit 127, `docker: command not found`, from the build step of the image job. The daemon was
never the problem and never missing: Testcontainers speaks to /var/run/docker.sock from a
.NET library, so every integration suite in the build job had been starting PostgreSQL,
Keycloak and an sshd on this runner while `docker` was not a command on it at all. Having a
socket and having a client are two different things to have, and this runner had one.

It failed late for the same reason it was easy to miss. Node, git, the SDK and the tags all
came up fine, so the job looked healthy right until the line that actually needed the
binary.

The client only. There is a daemon answering on that socket already — installing an engine
would start a second one beside the one in use, which is a worse outcome than the error.

Verified by running this step's own script in an Alpine container with the socket mounted:
it installs docker-cli, the client then reports server 29.6.2 across the socket, and the
API image builds to completion from inside that container with the repository as its
context. Which is as close to the runner as this can be checked without being it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 15:14:28 +02:00
co-authored by Claude Opus 5
parent 338c1a8647
commit 5ddbca49d3
+34
View File
@@ -337,6 +337,40 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# The daemon and the client are two different things to have, and this runner had only
# one of them. Testcontainers reaches Docker straight over /var/run/docker.sock from a
# .NET library, so every integration suite in the build job passed while `docker` was
# not a command here at all — which surfaced as exit 127 from the build step below,
# after the tags had been worked out and everything looked healthy.
#
# Only the CLI. The socket is already there and a daemon is already answering on it;
# installing an engine would start a second one beside the one being used.
- name: ensure the docker cli
run: |
set -eu
SUDO=""
[ "$(id -u)" -eq 0 ] || SUDO="sudo"
if command -v docker >/dev/null 2>&1; then
docker --version
exit 0
fi
echo "Installing the docker cli"
if command -v apk >/dev/null 2>&1; then
$SUDO apk add --no-cache docker-cli
elif command -v apt-get >/dev/null 2>&1; then
$SUDO apt-get update -qq
$SUDO apt-get install -y --no-install-recommends docker.io
elif command -v dnf >/dev/null 2>&1; then
$SUDO dnf install -y docker-cli
else
echo "No apt-get, apk or dnf here, so the docker client cannot be installed from" >&2
echo "inside the job. Add it to the runner's image." >&2
exit 1
fi
docker --version
# No docker/* actions here, deliberately. The build is single-architecture, so it # No docker/* actions here, deliberately. The build is single-architecture, so it
# needs the daemon this runner already has for the Testcontainers suites and nothing # needs the daemon this runner already has for the Testcontainers suites and nothing
# else — no buildx plugin, no QEMU, and no third-party action whose SHA has to be # else — no buildx plugin, no QEMU, and no third-party action whose SHA has to be