Skip to content

Converting an Existing Workflow

Most of a working workflow needs no changes. What breaks depends entirely on the workload’s runtime tier: on dind or isolated-sysbox a job’s containers are children of the runner’s own daemon and almost everything behaves as it did on ubuntu-latest. On shared-daemon the daemon belongs to the host, so containers the job starts are its siblings, and that single fact is behind every item below. isolated-kata runs no daemon at all, so a job that needs Docker doesn’t belong there.

  1. Point runs-on at the workload’s labels. self-hosted is implied by every Runaway runner; add it alongside the workload’s own label. See routing.
  2. Put toolchain actions first. actions/setup-node, setup-python and friends should run before any step that shells out to node, npm or python. The Standard runner image’s system interpreters are older than a hosted runner’s, and an installer that runs before setup-node runs on whatever the image ships. See the Standard image.
  3. Drop the GitHub cache actions. actions/cache and cache: inputs on setup actions round-trip through GitHub’s Cache service; /cache is already warm and local. See Caching.

That’s the whole list for none, dind and isolated-sysbox — and for isolated-kata, provided the job needs no Docker.

  1. Replace services:. The runner publishes a service container’s ports on the host and then looks for them on its own localhost, where nothing is listening. Start the container yourself on a per-run network and join the runner to it:

    - name: Start Postgres
    run: |
    docker network create "ci-$GITHUB_RUN_ID"
    docker run -d --name "postgres-$GITHUB_RUN_ID" \
    --network "ci-$GITHUB_RUN_ID" --label "ci-run=$GITHUB_RUN_ID" \
    -e POSTGRES_PASSWORD=ci postgres:17-alpine
    docker network connect "ci-$GITHUB_RUN_ID" "$(cat /etc/hostname)"

    A container: job has no conversion. The runner injects its own Node into the job container from a path that exists only inside the runner image, and the host’s daemon can’t see it, so JavaScript actions fail before your steps run. Move those jobs to a nested tier.

  2. Replace every localhost with a container name. Anything the job reaches over the network — a database URL, a health check, a smoke test — addresses the container by name on that network. Published ports are not how the job gets in; network membership is.

  3. Scope container names and image tags to the run. A fixed --name or -t is global on a shared daemon, so two concurrent jobs collide. Use $GITHUB_RUN_ID in both.

  4. Add a teardown step with if: always(). The runner is ephemeral; the daemon is not. Anything the job started outlives it unless the job removes it. See hygiene.

  5. Check what the job bind-mounts. Paths outside $GITHUB_WORKSPACE don’t exist on the host and mount empty. See bind mounts.

  6. Give docker build a route to any service it needs. BuildKit won’t join a user-defined network. See BuildKit networking.

Worth knowing so you don’t rewrite more than you need to:

  • Every action from the marketplace, including actions/checkout and upload-artifact.
  • Matrix builds, concurrency, if: conditions, reusable workflows, and job outputs.
  • Anything that runs directly on the runner — compilers, test suites, linters, and a server the job starts and then talks to on its own localhost.
  • Secrets, GITHUB_TOKEN, and $GITHUB_ENV / $GITHUB_OUTPUT.

Converting a workflow for shared-daemon makes it specific to a runner with a host socket. It will no longer run on ubuntu-latest, because docker network connect … $(cat /etc/hostname) has no meaning there. If you need a workflow that runs in both places, use a nested tier instead and keep services:.