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.
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-latestorwindows-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:
action.yml: The metadata manifest declaringruns.using: 'docker'.Dockerfile: Defines the container image build steps, base image, and installed packages.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:
- The input name is converted to UPPERCASE.
- Any hyphens (
-) or spaces are replaced with underscores (_). - 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 Path | Host Source Path | Purpose & Access |
|---|---|---|
/github/workspace | $GITHUB_WORKSPACE | The 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_workflow | Workflow runtime state and event JSON payloads ($GITHUB_EVENT_PATH). |
/github/file_commands | $RUNNER_TEMP/_github_home | Mount point for $GITHUB_OUTPUT, $GITHUB_ENV, and $GITHUB_PATH files. |
[!IMPORTANT] Root Permissions: Docker container actions run as the
rootuser (UID 0) by default inside the container. Any files created inside/github/workspaceby the container will be owned byroot. 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 Dimension | Local Dockerfile (image: 'Dockerfile') | Prebuilt Image (image: 'docker://<uri>') |
|---|---|---|
| Image Source | Built dynamically on the runner at job startup | Pulled directly from a container registry (Docker Hub, GHCR, ECR) |
| Startup Overhead | High (Runs docker build on every job execution) | Low / Moderate (Fast docker pull of pre-compiled layers) |
| Build Consistency | Can break if upstream base packages or network repos change | Deterministic (Immutable image digest/tag ensures consistency) |
| Maintenance Overhead | Minimal (Only maintain Dockerfile in Git repo) | Requires a separate CI/CD release workflow to build and push images |
| Private Registry Auth | Not applicable | Requires credentials if pulling from private registry registries |
| Best For | Fast prototyping, simple lightweight Dockerfiles | Production enterprise actions, heavy toolchains, complex compilers |
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 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?
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?