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 — 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.