1. Introduction

GitHub Actions is a tool that enables us to automate tasks within a software development workflow directly on GitHub. It’s excellent at automating the build, test, and deployment process, which is often referred to as continuous integration and continuous delivery (CICD).

GitHub Actions workflows define jobs, which are further broken down into steps. These steps involve running scripts using pre-built actions. Furthermore, jobs run directly on the runner’s machine environment. Containerized jobs or steps, however, add a layer of isolation. Each job or step runs within its own Docker Container, ensuring it has all the necessary tools without affecting the runner.

In this tutorial, we’ll explore possible ways to use Docker with GitHub Actions. All commands have been tested on Ubuntu 22.04.

2. Why Use Docker Containers in GitHub Actions

There are several advantages to using Docker containers in GitHub Actions workflows:

  • Consistent environment: eliminates issues arising from missing dependencies or conflicting versions on the runner machine, enabling the code to always run in the same predictable environment regardless of the runner used
  • Reproducible builds: containers capture the entire environment, so the build process produces the same results on the local machine, the CI server, or any other environment using the same container image
  • Job isolation: containerization prevents conflicts between jobs that might use different tools or libraries by preventing unwanted interaction or overlap, thereby protecting the runner machine from any potential issues within a specific job
  • Simplify workflows: instead of managing dependencies on the runner machine, we can specify the Docker image containing the desired environment, which usually makes the workflow more declarative and easier to understand and maintain

Overall, Docker containers in GitHub Actions provide a way to achieve consistent, isolated, and reusable workflows.

3. Getting Started With Docker in GitHub Actions

Docker in GitHub Actions is achievable in two ways:

  • Using a Docker container for a complete GitHub Actions job
  • Using a step to refer to an action configured to run in a container

Regardless of which method we use, we get isolated environments. Notably, a Linux runner must be used for a GitHub Action workflow to use Docker containers.

For this tutorial, let’s create a GitHub repository named docker-github-actions:

Github Repository for Docker in Github Actions

Then, we clone the repository to a local machine with git preinstalled:

$ git clone https://github.com/<username>/docker-github-actions.git

Using the terminal, let’s navigate to the project path and create a .github/workflows directory:

$ cd docker-github-actions
$ mkdir -p .github/workflows

GitHub Actions workflows reside in the project repository under .github/workflows. It’s crucial to store all workflow definition files within this directory so that GitHub Actions can recognize and process them.

4. Using a Docker Container for a Complete GitHub Actions Job

After we’ve successfully created the workflows directory, let’s define a workflow that runs a complete GitHub Actions job in a container.

This method involves defining a container to run all the job steps within it. We define a job using the jobs keyword in the GitHub workflow YAML file. Then, within this job definition, we specify that we want to use a container by adding the container keyword.

4.1. Defining Container Jobs in YAML GitHub Action Workflow

However, including the container keyword in a job doesn’t explicitly mean the job steps run in a container. To run job steps inside a container, we specify both the host machine and the container image:

jobs:
  container-test-job:
    runs-on: ubuntu-latest
    container: node:18

The example above defines the host machine as ubuntu-latest and the container image as node:18 using the runs-on and container keywords, respectively. If the container image registry requires authentication, we can use the credentials keyword in the workflow file to provide a map containing the username and password:

container:
  image: ghcr.io/owner/image
  credentials:
     username: ${{ github.actor }}
     password: ${{ secrets.github_token }}

Additionally, GitHub Actions enable us to define environment variables and ports exposed on the container using the env and ports keywords, respectively:

container:
  image: python:3.9
  env:
     TEST_VAR: "This is a test environment variable"
   ports:
      - 8000:8000

Also, GitHub Actions enable mounting volumes onto the container. As usual, we can link directories on the host machine with paths in the container or share data between services and other steps in a job using the volumes keyword:

volumes:
  - my_docker_volume:/volume_mount
  - /data/my_data
  - /source/directory:/destination/directory

As seen in the example above, a semicolon (:) separates the source path from the destination path in the container.

4.2. Running a Job Within a Container

For demonstration purposes, let’s create an example workflow that runs a job using a container image based on Node.js version 17.6.0. The job steps verify the presence and version of both Node.js and npm within the container environment.

To do this, we begin by creating a container-job.yaml file in the workflows directory and enter the workflow configuration:

on: push
jobs:
    container-job:
        runs-on: ubuntu-latest
        container: node:17.6.0
        steps:
            - run: node --version
            - run: npm --version

Next, we add and push the configuration to the remote repository using git:

$ git add .
$ git commit -m "Run Node 17"
$ git push

Now, there’s a new workflow run with a commit message Run Node 17 in GitHub Actions:

Workflow Run with Commit Message: Run Node 17

In this workflow run, we see the container-job job with a green checkmark, meaning it was successful:

Container-Job Job With a Green Checkmark

Selecting this job, we observe the container initialization process and the steps ran inside the container:

Container-Job Container Initialization Process and Step Execution

As seen above, the steps are executed within the container as they display the expected Node.js (17.6.0) and npm versions.

5. Use Dockerfile for a Container Action in a Step

The Dockerfile method involves GitHub Actions creating a container image based on the provided Dockerfile, and then spawning a container from that image.

To define inputs and code for execution within the container, we create two files:

  • action metadata YAML file
  • action code

GitHub Actions enables us to employ the completed Docker container action in a subsequent step via the uses keyword.

5.1. Creating Dockerfile

To begin with, let’s create a Dockerfile in the project directory. After that, we insert the Docker container image configuration:

FROM alpine:3.10

COPY entrypoint.sh /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

This Dockerfile creates an image based on Alpine Linux, copies the entrypoint.sh script into the container, and configures the container to run that script upon startup.

5.2. Creating an Action Metadata

Now, let’s create a custom action that runs a greeting logic in the container defined in the Dockerfile.

To do this, we create an action.yml file in the project directory. Then, we enter the logic configuration within action.yml:

# action.yml
name: 'Hello Baeldung'
description: 'Greet Baeldung and record the time'
inputs:
  who-to-greet:  # id of input
    description: 'Who to greet'
    required: true
    default: 'Baeldung'
outputs:
  time: # id of output
    description: 'The time we greeted Baeldung'
runs:
  using: 'docker'
  image: 'Dockerfile'
  args:
    - ${{ inputs.who-to-greet }}

The above metadata tells GitHub Actions how to run the code. It defines the who-to-greet input and time output. Then, it instructs GitHub Actions to build an image from the Dockerfile and run commands in a new container using this image.

5.3. Implementing the Action Code

Let’s create a shell script that uses the input variable, who-to-greet, as declared in the previous step, to print “*Hello *” in the logs.

Next, the script captures the current time and stores it as an output variable that future actions in the job can use.

As declared earlier in the Dockerfile, let’s create the entrypoint.sh file. Then, we input the action code script within it:

#!/bin/sh -l

echo "Hello $1"
time=$(date)
echo "time=$time" >> $GITHUB_OUTPUT

Once saved, the file has to be made an executable:

$ chmod u+x entrypoint.sh

This way, we ensure entrypoint.sh can be run.

5.4. Testing Action in a Workflow

Since we’ve implemented the Docker container action and action code, let’s set up a GitHub workflow to use them.

Let’s set up the workflow configuration in .github/workflows/step-action.yml:

on: [push]

jobs:
  hello_baeldung_job:
    runs-on: ubuntu-latest
    name: A job to say hello to Baeldung
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Hello Baeldung action step
        uses: ./ # Uses an Docker container action in the root directory
        id: hello
        with:
          who-to-greet: 'Baeldung'
      - name: Get the output time
        run: echo "The time was ${{ steps.hello.outputs.time }}"

In the workflow above, the Hello Baeldung action step uses the Docker container action in the root directory created in the previous steps.

Now, we can push all the created actions, code, and workflow to the remote repository:

$ git add .
$ git commit -m "Docker container step action"
$ git push

Once pushed, the repository displays a new workflow run with a commit message Docker container step action. Then, under Jobs, the job titled A job to say hello to Baeldung can be seen to have completed successfully, as indicated by the green check mark:

A Job to Say Hello to Baeldung

Selecting the job, we can see the Hello Baeldung action step that prints Hello Baeldung:

A Job to Say Hello to Baeldung Step Action and Echo Output

As seen above, the workflow builds the Docker image and creates a container to execute the step action in a single, automated sequence.

6. Using addnab/docker-run-action for a Container Action in a Step

While it’s a common method to build a Docker container action from scratch and then use it in a step, there are also publicly available Docker container actions available on the GitHub marketplace. For this tutorial, we demonstrate addnab/docker-run-action.

6.1. Creating Workflow

To demonstrate, we create a workflow that executes a step on an nginx Docker container image made available through addnab/docker-run-action.

Let’s create the workflow configuration in github/workflows/public-action.yml:

name: Run step in Docker

on: [push]

jobs:
    container-job:
        runs-on: ubuntu-latest
        steps:
            - name: Checkout code
              uses: actions/checkout@v2
        
            - name: Run step in container
              uses: addnab/docker-run-action@v3
              with:
                image: nginx:latest
                run: |
                  echo "This step is using a Public Docker Container Action "

This workflow checks out the code and then executes an echo command within a container based on the nginx:latest image.

6.2. Testing Workflow

Since we already have the workflow, let’s add and push it up to the remote repository:

$ git add .
$ git commit -m "Public Docker container action"
$ git push

Again, in the Actions tab in the repository, a new workflow named Run step in Docker appears as specified in the configuration. Selecting the job, we can see the echo output in the logs:

addnab/docker-run-action Echo Log Output

Hence, the step has run on the Docker container.

7. Conclusion

In this article, we’ve learned how to use Docker in GitHub Actions, either by running an entire job in a container or referring to a configured action in a job step.

In conclusion, Docker in GitHub Actions offers a flexible toolkit for efficient workflows. Whether we require a fully containerized job or reusable Docker container actions for streamlined execution, the GitHub Actions feature enables us to improve the development process.