5.4 Building Docker Container Actions

Key Takeaways

  • Docker container actions encapsulate execution logic, binaries, and operating system packages within a Linux container using runs.using: 'docker'.
  • Docker container actions can execute ONLY on Linux runners (such as ubuntu-latest or Linux self-hosted runners); attempting to run them on macOS or Windows hosted runners results in an immediate fatal error.
  • The image: parameter can specify a local Dockerfile (image: 'Dockerfile') built at runtime or a prebuilt container image from a public registry (image: 'docker://alpine:3.18').
  • The runner automatically maps declared action inputs to uppercase environment variables prefixed with INPUT_ (e.g., input target_env becomes $INPUT_TARGET_ENV), and actions write outputs to $GITHUB_OUTPUT.
  • The runner workspace ($GITHUB_WORKSPACE) is mounted into the container at /github/workspace, which serves as the default working directory.
Last updated: August 2026

Building Docker Container Actions

Docker Container Actions package your action's code, runtime environment, system binaries, and operating system dependencies into a self-contained Linux container. While JavaScript and Composite actions rely on tools pre-installed on the host runner, Docker actions allow you to control the exact Linux distribution, system libraries, CLI utilities, and compiler versions required by your automation.

Docker container actions are ideal for running tasks that require bespoke Linux software, specific C/C++ toolchains, custom Python/Ruby/Go environments, or strict filesystem isolation. However, they come with distinct performance trade-offs and operating system boundaries that are heavily emphasized on the GH-200 examination.


1. Operating System Boundary: The Linux-Only Restriction

The single most critical architectural constraint for Docker container actions is runner compatibility:

+-----------------------------------------------------------------------------+
|                   DOCKER CONTAINER ACTION OS COMPATIBILITY                  |
|                                                                             |
|   +---------------------------------+-----------------------------------+   |
|   | RUNNER OPERATING SYSTEM         | EXECUTION STATUS                  |   |
|   +---------------------------------+-----------------------------------+   |
|   | Ubuntu Linux (ubuntu-latest)    | [SUCCESS] Native Docker daemon     |   |
|   | Linux Self-Hosted (with Docker) | [SUCCESS] Native Docker daemon     |   |
|   | macOS (macos-latest / macos-14) | [FATAL ERROR] Container unsupported|   |
|   | Windows (windows-latest)        | [FATAL ERROR] Container unsupported|   |
|   +---------------------------------+-----------------------------------+   |
+-----------------------------------------------------------------------------+

[!CAUTION] Linux Runner Requirement: Docker container actions can only run on Linux runners. GitHub-hosted macOS and Windows runners do not support running Docker container actions. If a workflow step executes a Docker container action on macos-latest or windows-latest, the runner fails immediately with: Container action is only supported on Linux.


2. Anatomy of a Docker Container Action

A Docker container action repository typically consists of three core files:

  1. action.yml: The metadata manifest declaring runs.using: 'docker'.
  2. Dockerfile: Defines the container image build steps, base image, and installed packages.
  3. entrypoint.sh: The shell script executed when the container starts.
+-----------------------------------------------------------------------------+
|                        DOCKER ACTION DIRECTORY STRUCTURE                    |
|                                                                             |
|   custom-docker-action/                                                     |
|   ├── action.yml               # Manifest declaring inputs, outputs & docker|
|   ├── Dockerfile               # Base image & toolchain setup               |
|   ├── entrypoint.sh            # Runtime execution script                   |
|   └── README.md                # Usage documentation                        |
+-----------------------------------------------------------------------------+

The action.yml Specification

name: 'Terraform Security Scanner'
description: 'Runs tfsec and Checkov inside an isolated Alpine security container'

inputs:
  scan-directory:
    description: 'Path to Terraform configuration files'
    required: false
    default: '.'
  severity-threshold:
    description: 'Minimum severity level to trigger failure (LOW, MEDIUM, HIGH, CRITICAL)'
    required: true
    default: 'HIGH'

outputs:
  violations-count:
    description: 'Total number of detected security violations'

runs:
  using: 'docker'
  image: 'Dockerfile'              # Or prebuilt image: 'docker://alpine:3.18'
  entrypoint: '/entrypoint.sh'     # Overrides Dockerfile ENTRYPOINT
  args:                            # Passed as arguments to entrypoint
    - ${{ inputs.scan-directory }}
    - ${{ inputs.severity-threshold }}

3. The Dockerfile and entrypoint.sh Suite

The Dockerfile Implementation

# Use minimal, secure Alpine base image
FROM alpine:3.19

# Install runtime dependencies
RUN apk add --no-cache \
    bash \
    curl \
    jq \
    git

# Copy entrypoint script into container root
COPY entrypoint.sh /entrypoint.sh

# Ensure entrypoint script is executable
RUN chmod +x /entrypoint.sh

# Define container entrypoint
ENTRYPOINT ["/entrypoint.sh"]

The entrypoint.sh Script

#!/usr/bin/env bash
set -euo pipefail

# 1. Access arguments passed via args: [] or input environment variables
SCAN_DIR="${1:-$INPUT_SCAN_DIRECTORY}"
SEVERITY="${2:-$INPUT_SEVERITY_THRESHOLD}"

echo "Starting Security Scan in directory: ${SCAN_DIR}"
echo "Enforcing Severity Threshold: ${SEVERITY}"

# 2. Perform scanning operations inside mounted workspace (/github/workspace)
cd "/github/workspace/${SCAN_DIR}"

VIOLATIONS_FOUND=0
# (Scanning logic executed here...)

# 3. Export outputs via GITHUB_OUTPUT environment variable
if [ -n "${GITHUB_OUTPUT:-}" ]; then
  echo "violations-count=${VIOLATIONS_FOUND}" >> "$GITHUB_OUTPUT"
fi

# 4. Handle exit codes
if [ "$VIOLATIONS_FOUND" -gt 0 ]; then
  echo "::error::Detected ${VIOLATIONS_FOUND} security violations exceeding threshold ${SEVERITY}"
  exit 1
fi

echo "Security audit completed successfully with 0 violations."
exit 0

4. Input Translation: The INPUT_<NAME> Convention

When a workflow step invokes a Docker container action with inputs, the GitHub Actions runner automatically converts those inputs into environment variables passed directly into the container process:

+-----------------------------------------------------------------------------+
|                        INPUT TO ENVIRONMENT MAPPING                         |
|                                                                             |
|   Caller Workflow with: Block             Container Environment Variable    |
|   ---------------------------             ------------------------------    |
|   with:                                                                     |
|     scan-directory: './infra'   =====>    $INPUT_SCAN_DIRECTORY="./infra"   |
|     severity-threshold: 'HIGH'  =====>    $INPUT_SEVERITY_THRESHOLD="HIGH"  |
|     api_key: 'secret123'        =====>    $INPUT_API_KEY="secret123"        |
+-----------------------------------------------------------------------------+

Transformation Rules:

  1. The input name is converted to UPPERCASE.
  2. Any hyphens (-) or spaces are replaced with underscores (_).
  3. The variable is prefixed with INPUT_.

Inside your entrypoint.sh script, you can read inputs either by positional arguments ($1, $2) if defined in args: [], or directly from $INPUT_<PARAMETER_NAME>.


5. Container Filesystem Mounts & Permissions

When GitHub Actions runs a Docker action, it mounts specific host runner directories into the container to enable file access and communication with the runner daemon:

Container Mount PathHost Source PathPurpose & Access
/github/workspace$GITHUB_WORKSPACEThe checked-out repository code. The container's default working directory is set to /github/workspace.
/github/home$HOME (Runner user home)Preserves user profile configuration and tool settings.
/github/workflow$RUNNER_TEMP/_github_workflowWorkflow runtime state and event JSON payloads ($GITHUB_EVENT_PATH).
/github/file_commands$RUNNER_TEMP/_github_homeMount point for $GITHUB_OUTPUT, $GITHUB_ENV, and $GITHUB_PATH files.

[!IMPORTANT] Root Permissions: Docker container actions run as the root user (UID 0) by default inside the container. Any files created inside /github/workspace by the container will be owned by root. If subsequent non-container steps on the host runner attempt to modify or delete these files, permission errors can occur unless ownership is adjusted.

6. Local Dockerfile vs Prebuilt Registry Images

In action.yml, the image: key supports two distinct modes of container resolution:

# Mode 1: Local Dockerfile build
runs:
  using: 'docker'
  image: 'Dockerfile'

# Mode 2: Prebuilt registry image
runs:
  using: 'docker'
  image: 'docker://ghcr.io/enterprise-org/sec-scanner:v2.1.0'

Comprehensive Comparison Table

Evaluation DimensionLocal Dockerfile (image: 'Dockerfile')Prebuilt Image (image: 'docker://<uri>')
Image SourceBuilt dynamically on the runner at job startupPulled directly from a container registry (Docker Hub, GHCR, ECR)
Startup OverheadHigh (Runs docker build on every job execution)Low / Moderate (Fast docker pull of pre-compiled layers)
Build ConsistencyCan break if upstream base packages or network repos changeDeterministic (Immutable image digest/tag ensures consistency)
Maintenance OverheadMinimal (Only maintain Dockerfile in Git repo)Requires a separate CI/CD release workflow to build and push images
Private Registry AuthNot applicableRequires credentials if pulling from private registry registries
Best ForFast prototyping, simple lightweight DockerfilesProduction enterprise actions, heavy toolchains, complex compilers
Test Your Knowledge

A DevOps engineer creates a workflow that tests a multi-platform application across three matrix runner environments: ubuntu-latest, windows-latest, and macos-latest. The job includes a step invoking a Docker container action: uses: my-org/custom-docker-action@v1. What happens when the workflow runs across the matrix?

A
B
C
D
Test Your Knowledge

A custom Docker container action defines an input named target-environment in action.yml. Inside the action's entrypoint.sh script, how should this input value be accessed directly from the environment?

A
B
C
D
Test Your Knowledge

An enterprise DevOps team is experiencing 4-minute job delays on their CI workflows because their custom Docker action uses image: 'Dockerfile', forcing the runner to download large package dependencies and build the container from scratch on every run. What architectural change will most effectively reduce step execution latency while maintaining container isolation?

A
B
C
D