open atlas
↑ Back to track
CI/CD pipelines CICD · 09 · 03

Docker actions: total environment control at the cost of Linux-only and image overhead

A Docker action packages its entire environment in a container — any language, any system dependency — but runs only on Linux runners and pays a 30-90s image pull or build per run, versus a composite or JS action measured in milliseconds.

CICD Senior ◷ 16 min
Level
FoundationsJuniorMiddleSenior

The team needed a release action that ran a Go binary, cosign, and a pinned version of skopeo — a toolchain that did not exist as npm packages and would have meant scripting fragile downloads in a composite action. So they reached for a Docker action: a Dockerfile that FROM-ed a known base, installed the exact tools, and ran an entrypoint. It worked perfectly. Then they tried to add the action to a workflow that ran on macos-latest for a separate signing requirement, and the job failed before the entrypoint ever ran: Docker container actions are only supported on Linux runners. And on the Linux jobs, the action that had felt fast in testing now added a flat 40-70 seconds to every run — the runner was building the image from the Dockerfile on each invocation because nothing pre-built it. The container had bought them an arbitrary environment, and charged them an arbitrary-environment’s startup tax and OS lock-in on every single run.

By the end of this lesson you will know exactly when Docker is the only viable action type, what the two costs are that you pay on every single run, and how to halve the startup tax without abandoning the container.

Total environment control

When composite and JavaScript actions fall short — because you need cosign, a specific glibc, or a compiled binary with no npm equivalent — the Docker action is not a workaround: it is the only type designed to own its entire environment. The question is not whether Docker is powerful enough; it is whether the cost is worth it for your use case.

A Docker action declares runs.using: "docker" and either image: "Dockerfile" (built on the runner) or image: "docker://ghcr.io/org/img:tag" (pulled pre-built). Inside the container you control everything — base OS, system libraries, any language runtime, exact tool versions — which is the one thing composite and JavaScript actions cannot give you. If your action needs cosign, a specific glibc, a compiled Go or Rust binary, or a tool with no npm equivalent, the container is the only type that packages it reproducibly:

FROM alpine:3.20
RUN apk add --no-cache cosign skopeo
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
# action.yml
runs:
  using: "docker"
  image: "Dockerfile"
  args:
    - ${{ inputs.tag }}
  env:
    REGISTRY_TOKEN: ${{ inputs.token }}

The runtime contract differs from a JavaScript action. Inputs arrive as INPUT_* environment variables in the container (an input tag becomes INPUT_TAG), and you can also pass them positionally via args:. Outputs are written by appending to the file named by $GITHUB_OUTPUTecho "result=ok" >> "$GITHUB_OUTPUT" from inside the entrypoint — and the workspace is bind-mounted at $GITHUB_WORKSPACE. There is no toolkit; you speak the protocol directly through env vars and these files.

Quiz

A team adds a working Docker container action to a job that runs on `runs-on: macos-latest`. The job fails before the action's entrypoint executes. What is the cause?

The cost: Linux-only and image overhead

The container’s power has two prices, both paid on every run. First, Docker actions run only on Linux runners — not macOS, not Windows. An action you want usable everywhere cannot be a Docker action; that alone pushes many reusable actions toward JavaScript. Second, the container has to materialize before your code runs. With image: "Dockerfile", the runner builds the image on every invocation unless layers are cached — commonly 40-90 seconds. With image: "docker://...", it pulls a pre-built image — faster, but still tens of seconds for a cold pull of a non-trivial image. Compare that to a composite (milliseconds) or a JavaScript action (sub-second node boot): the Docker action adds a flat, per-run tax that is invisible in a long job and brutal in a fast one triggered hundreds of times a day.

When you add a Docker action to a fast workflow triggered hundreds of times a day, ask yourself: does the logic actually require an arbitrary container environment, or could a JavaScript action handle it? If the answer is yes, it needs Docker — then the next question is how to cut the image startup cost. The mitigation is to publish a pre-built image to a registry (GHCR) and reference it with docker://, turning a per-run build into a per-run pull and letting layer caching help. But that adds a release pipeline for the image itself. Two security notes inherit from the container world and matter more here than for the other types: pin the base image by digest (FROM alpine:3.20@sha256:...) so the build is reproducible and a re-tagged upstream base can’t change your action silently; and pin any actions your workflow uses to invoke this one by commit SHA, the same supply-chain hygiene as the other action types. The decision rule is blunt: choose Docker only when you genuinely need an environment composite and JavaScript cannot provide, and accept Linux-only plus the startup tax as the cost of that control.

Quiz

A Docker action using `image: "Dockerfile"` adds ~60 seconds to every run of a workflow that fires hundreds of times a day. The container's logic is fast. What is the most effective fix that keeps it a Docker action?

Recall before you leave
  1. 01
    What is the unique capability of a Docker action, and the two recurring costs that come with it?
  2. 02
    How do inputs and outputs work for a Docker action, and how do you reduce its per-run startup cost?
Recap

A Docker action sets runs.using: "docker" and points image: at either a Dockerfile (built on the runner) or a docker:// reference to a pre-built image, and inside that container you control the entire environment: base OS, system libraries, any language runtime, exact tool versions, and tools with no npm equivalent. That is the one capability composite and JavaScript actions structurally cannot provide, and it is the only reason to choose Docker. The cost is charged on every single run and comes in two parts. First, Docker container actions run only on Linux runners — not macOS, not Windows — so an action meant to be usable everywhere cannot be a Docker action, which alone pushes most general-purpose reusable actions toward JavaScript. Second, the image has to materialize before your entrypoint runs: building from image: "Dockerfile" adds roughly 40-90 seconds per run unless cached, and even a pre-built docker:// image costs tens of seconds on a cold pull — a flat tax that disappears inside a long job and dominates a fast one triggered hundreds of times a day, against composite’s milliseconds and a JS action’s sub-second boot. The runtime protocol is bare: inputs arrive as INPUT_* environment variables (and optional positional args:), outputs are appended to $GITHUB_OUTPUT, the repo is mounted at $GITHUB_WORKSPACE, and there is no toolkit. Mitigate the startup tax by publishing a pre-built image to a registry and referencing it with docker://, turning a per-run build into a per-run pull with layer caching, and pin the base image by digest so the build is reproducible and a re-tagged upstream base cannot silently change your action. The decision rule: choose Docker only when you genuinely need an environment the other types can’t give, and accept Linux-only plus the image overhead as the deliberate price of that control. Now when you see a workflow job adding 60 seconds on every run for an action that “just runs a script,” you know to check whether it’s building a Dockerfile each time — and you know exactly which two levers to reach for.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.