The previous post was about pinning actions/checkout@v3 to keep builds
working on Debian Jessie and Stretch. That workaround had a ceiling.
Anything inside the runner container that depended on Node 20 (the Vault
action, the modern checkout, several other first-party actions) kept
breaking. This week I open-sourced a small action that flips the model:
keep the runner on a modern host, and run only the build commands inside
the legacy container.
The action is at github.com/andrius/run-in-docker.
Why Container Jobs Stopped Working
A GitHub Actions container job runs the workflow steps inside the
container itself. The runner downloads the Node-based actions, mounts them
into the container, and executes them with the container’s node binary.
As GitHub upgraded its first-party actions from Node 16 to Node 20, the
Node binary the runner expects to find inside the container had to keep
up. Debian Jessie’s glibc cannot run a Node 20 binary, so any container
job targeting Jessie now fails as soon as a Node-based action runs.
Pinning to Node 16 versions of the actions buys time, but it is a strict ceiling. Once the action we need (the Vault action, in this case) drops Node 16 support, the container job is dead.
The Action
run-in-docker is two small JavaScript actions: start and exec. They
run on the runner, which is a modern Ubuntu host where Node 20 is fine,
and they use the runner’s docker CLI to operate on the legacy container.
start pulls the image and starts a long-running container with the
workspace mounted, named so that subsequent steps can address it. exec
runs a shell snippet inside that container with docker exec. The
container itself only needs whatever the user wants to run there, a
Debian build toolchain for example. There is no Node binary inside the
container, no GitHub Actions runtime, and no glibc constraints inherited
from upstream actions.
The shape of a working job is straightforward:
- A modern Ubuntu runner executes the workflow.
actions/checkout@v4and the other Node 20 actions run on the runner, not in the container.run-in-docker/startbrings up the legacy Debian container.- One or more
run-in-docker/execsteps run the build, the tests and the packaging, which means anything that has to happen on Jessie or Stretch.
What It Carries Across
The action provides enough integration with GitHub Actions to feel like a
normal step. Workflow environment variables are passed into the
container. GITHUB_OUTPUT, GITHUB_ENV and GITHUB_STEP_SUMMARY are
writable from inside the container, so a build running on Jessie can set
step outputs and write to the job summary the same way a native step
would. The runner is reachable from the container as
host.docker.internal and host-gateway, so a service container started
by GitHub (a MariaDB, for example) is reachable by name from inside the
legacy build.
Usage
- uses: andrius/run-in-docker/start@main
with:
image: ghcr.io/example/buildpackage-generic-jessie:latest
container_name: debian-container
custom_options: >-
--add-host=mariadb:host-gateway
-e MYSQL_HOST=host.docker.internal
- uses: andrius/run-in-docker/exec@main
with:
container_name: debian-container
run: |
./build.sh
find . -name '*.deb' -exec mv {} ./deliverables/ \;
The full README has a longer example covering MariaDB seeding, artifact registry credentials and per-distribution matrices.
Open Source
An internal version of this action is in active use on the team’s repositories that target Debian Jessie, Stretch, Bullseye and Bookworm. The open-source version at andrius/run-in-docker is the same code, packaged for anyone hitting the same legacy Debian plus modern GitHub Actions constraint. Issues and PRs are welcome.