← Blog

A Run-in-Docker Action for GitHub Workflows

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@v4 and the other Node 20 actions run on the runner, not in the container.
  • run-in-docker/start brings up the legacy Debian container.
  • One or more run-in-docker/exec steps 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.