← 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, 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.