GitHub Actions let you automate your software development workflows directly within your GitHub repository. You can trigger these workflows on events like pushes, pull requests, or scheduled times. The most common way to build an action is by writing a script, but for more complex or reproducible tasks, you can package your action inside a Docker container. This ensures your action runs in a consistent environment, regardless of the runner’s OS or installed dependencies.
Let’s say we want to create a simple GitHub Action that greets the user and prints the commit message of the latest commit.
Here’s the action.yml file, which defines our action:
name: 'Docker Greeter Action'
description: 'A simple Docker-based action that greets and shows commit message'
inputs:
name:
description: 'Who to greet'
required: true
default: 'World'
outputs:
greeting:
description: 'The greeting message'
runs:
using: 'docker'
image: 'Dockerfile'
entrypoint: '/entrypoint.sh'
This action.yml tells GitHub Actions:
- The
nameanddescriptionof our action. - An
inputnamednamethat defaults to "World" if not provided. - An
outputnamedgreetingthat our action will produce. - Crucially,
using: 'docker'specifies that this action is containerized. image: 'Dockerfile'points to the Dockerfile that will build our container image.entrypoint: '/entrypoint.sh'specifies the script inside the container that will be executed.
Now, let’s create the Dockerfile that builds our action’s environment:
FROM alpine:latest
RUN apk add --no-cache bash
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
This Dockerfile is straightforward:
FROM alpine:lateststarts with a minimal Linux distribution.RUN apk add --no-cache bashinstalls Bash, which ourentrypoint.shscript will use.COPY entrypoint.sh /entrypoint.shcopies our script into the container.RUN chmod +x /entrypoint.shmakes the script executable.
Finally, here’s the entrypoint.sh script that contains the core logic:
#!/bin/bash
set -e
# Get inputs
GREETING_NAME="${INPUT_NAME:-World}" # Default to World if not provided
# Fetch the latest commit message
COMMIT_MSG=$(git log -1 --pretty=%B)
# Construct the greeting message
FULL_GREETING="Hello, $GREETING_NAME! The latest commit message was: $COMMIT_MSG"
# Set the output
echo "::set-output name=greeting::$FULL_GREETING"
echo "$FULL_GREETING"
This script:
- Uses
#!/bin/bashas its interpreter. set -eensures that the script exits immediately if any command fails.- It reads the
INPUT_NAMEenvironment variable, which GitHub Actions automatically sets based on ouraction.yml. IfINPUT_NAMEisn’t set, it defaults to "World". git log -1 --pretty=%Bis used to fetch the subject and body of the most recent commit. This command requires Git to be available in the container, which it is by default when running on a GitHub Actions runner.- It constructs the final greeting message.
echo "::set-output name=greeting::$FULL_GREETING"is the critical part for setting outputs. This special format is recognized by the GitHub Actions runner to capture values from your action.- The last
echocommand prints the greeting to the standard output, which will be visible in the action logs.
To use this action in a workflow, you would create a .github/workflows/main.yml file:
name: Docker Action Workflow
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Run Docker Greeter Action
id: greet
uses: ./
with:
name: GitHub User
- name: Print Greeting Output
run: echo "The action said: ${{ steps.greet.outputs.greeting }}"
In this workflow:
-
uses: ./tells GitHub Actions to use the action defined in the current repository’s root directory (where ouraction.ymlis located). -
with: name: GitHub Userpasses thenameinput to our action. -
id: greetassigns an ID to this step so we can reference its outputs. -
${{ steps.greet.outputs.greeting }}accesses thegreetingoutput set by our action.
When this workflow runs, the "Run Docker Greeter Action" step will build the Docker image from your Dockerfile, run the entrypoint.sh script inside the container, and then the "Print Greeting Output" step will display the captured greeting.
The most surprising thing about Docker-based actions is that you don’t explicitly need to docker build or docker push your image. GitHub Actions handles the image building and management for you when you use uses: ./ or uses: owner/repo@ref. It builds the image on the runner and caches it, making subsequent runs faster.
The primary benefit of using Docker for actions is consistency. Your action runs in an isolated, predictable environment. Any dependencies your action needs (like specific versions of libraries, tools, or even a particular OS flavor) are baked into the Docker image. This eliminates the "it works on my machine" problem and ensures your automation behaves the same way every time, everywhere. You can even use complex base images or install specific software within your Dockerfile without worrying about polluting the runner environment.
When you use uses: owner/repo@ref for a Docker action, GitHub Actions will automatically pull or build the image specified in the action.yml of that repository. It handles the lifecycle of the container for you.
The next challenge is managing more complex dependencies and build processes within your Docker image, potentially involving multi-stage builds to keep your final image lean.