Executing Arbitrary Docker Containers Within GitHub Actions Workflows

The native execution environment of GitHub Actions relies heavily on pre-configured runners, typically Ubuntu-based virtual machines. While the platform provides built-in mechanisms to execute steps within Docker containers, these default methods impose significant architectural constraints. Specifically, using the container key at the job level or the docker:// syntax in a step restricts the user’s ability to define custom network configurations, volume mounts, and entrypoint overrides. For complex continuous integration (CI) pipelines requiring specific runtime environments—such as static site generators needing simultaneous Node.js and PHP runtimes, or microservices requiring host networking—third-party actions have emerged to fill this gap. These tools allow engineers to invoke docker run with granular control, bridging the divide between standard GitHub Actions runners and fully customized containerized execution environments.

Limitations of Native GitHub Actions Container Support

GitHub Actions natively supports containerized steps through two primary mechanisms, both of which present distinct limitations for advanced use cases. The first method involves defining the container at the job level using the container key. This approach designates the specified Docker image as the base environment for every step within that job. While effective for isolating the entire job environment, it prevents the execution of intermediate steps that require a different context or host-level utilities without complex workarounds.

The second native method utilizes the docker:// URI scheme within a step definition, such as uses: docker://aschmelyun/cleaver. This approach treats the Docker image as an action in its own right. However, this syntax imposes strict requirements on the Docker image structure. To function correctly as a GitHub Action, the image must adhere to specific conventions, notably avoiding explicit WORKDIR and ENTRYPOINT definitions, as the GitHub Actions runner handles these internally. This restriction makes it difficult to repurpose existing, production-ready Docker images that rely on standard Dockerfile directives for their operation. Consequently, developers often face the dilemma of either modifying their production images to fit the GitHub Actions schema or accepting the lack of control over runtime arguments.

The Docker Container Action Approach

To address these limitations, the pl-strflt/docker-container-action provides a more flexible alternative. This third-party action is designed to run Docker containers with custom run options, supporting both pulling existing images and building new ones from scratch if they are not present in the registry. Unlike native methods, it allows for the specification of arbitrary docker run options, such as network modes and volume mounts, without requiring the image to conform to the GitHub Action specification.

When a step utilizing this action executes, it performs a registry check first. If the specified image exists in the configured registry (defaulting to ghcr.io), it pulls the image. If the image is absent, the action triggers a local build using the provided Dockerfile and the git repository context. Following this, it executes the container with the user-defined options. This behavior is particularly useful in composite actions, where a wrapper action can infer the repository and reference details from the GitHub environment and then delegate the actual execution to the Docker Container Action.

The action accepts a comprehensive set of inputs to configure the build and run phases. Key inputs include repository and ref, which define the source of the Dockerfile and context. The image and tag inputs allow for explicit image naming, defaulting to the repository and ref respectively. Critical for advanced configurations are the opts and args inputs. The opts input accepts standard Docker run options, such as --network=host, enabling direct access to the host's network stack. The args input allows for the specification of runtime arguments, effectively overriding the image's default command. Additionally, build-args enables the injection of build-time variables, ensuring that the resulting image is tailored to the specific workflow requirements.

yaml - name: Run Docker container uses: pl-strflt/docker-container-action@v1 with: repository: owner/repo ref: main opts: --network=host build-args: | CUSTOM_ARG_1:foo CUSTOM_ARG_2:bar

The action also provides flexibility in handling exit codes and working directories. The allow-exit-codes input accepts a comma-separated list of exit codes that should be considered successful, with the wildcard * allowing all exit codes to pass. This is valuable for testing scenarios where specific non-zero exit codes are expected. The working-directory input defaults to ${{ github.workspace }}, but can be overridden to specify a different execution context within the container. The action outputs the results of the Docker run step in JSON format, facilitating downstream processing within the workflow.

Alternative Solutions: Run Docker Container Action

Another prominent solution in the ecosystem is the tonys-code-base/run-container-action. This action focuses on the straightforward execution of Docker images sourced from Docker Hub or Amazon ECR. It distinguishes itself by separating Docker run options from runtime arguments, providing a clear interface for mounting volumes and passing environment variables.

This action requires explicit authentication to the Docker registry, typically achieved by preceding the run step with the docker/login-action. This separation of concerns ensures that credential management is handled distinctly from the execution logic. The action supports standard Docker run options through the options input, such as volume mounts (-v) and environment variable definitions (-e). The runtime_args input allows for the specification of the command to execute within the container, such as sh -c "/myapp/path/my-script.sh".

A notable feature of this action is its output of the Docker run return code via docker_run_rc. This enables subsequent steps to inspect the exit status of the container, implementing conditional logic based on the success or failure of the executed command. This is particularly useful in workflows where the container's exit code carries semantic meaning beyond simple success or failure, such as in diagnostic tools or specific testing frameworks.

yaml - name: Run container id: run-docker-container uses: tonys-code-base/[email protected] with: docker-registry-url: ${{ env.registry }} image: ${{ env.image_name }} tag: ${{ env.image_tag }} options: >- -v "${{ github.workspace }}":"/myapp/path" -e MYENV_VAR=sample-value runtime_args: >- sh -c "/myapp/path/my-script.sh"

Real-World Application: Static Site Generation

The utility of these third-party actions is best illustrated by practical use cases, such as automating the build and deployment of static sites. Consider a scenario involving a static site generator like Cleaver, which requires both Node.js and PHP runtimes to compile assets and build the site. While GitHub Actions runners include support for both Node.js and PHP, maintaining a consistent environment across different deployment targets is often achieved through Docker images.

Using a native approach, a developer might be forced to create a specialized Docker image that adheres to the GitHub Action specifications, stripping away standard Dockerfile directives. Alternatively, by utilizing an action like addnab/docker-run-action, the developer can leverage their existing production image. The workflow checks out the repository, then executes a step that runs the specific Docker image with the necessary volume mounts to access the workspace.

yaml - name: Run the build process with Docker uses: addnab/docker-run-action@v3 with: image: aschmelyun/cleaver:latest options: -v ${{ github.workspace }}:/var/www run: | composer install npm install npm run production

In this configuration, the options input mounts the GitHub workspace into the container's /var/www directory. The run input specifies a multi-line command block that executes the installation and build processes. This approach ensures that the build environment is identical to the production environment, eliminating discrepancies caused by differences in runner configurations. It also allows for the use of complex build commands that would be difficult to encapsulate in a single GitHub Action entrypoint.

Integration with Docker Hub and CI/CD Pipelines

Beyond running containers for build steps, GitHub Actions plays a critical role in the continuous integration and deployment of Docker images themselves. The official Docker documentation outlines workflows for building, testing, and pushing images to Docker Hub. This process requires secure authentication to the registry.

To implement this, developers must create a Docker access token and store it as a secret in their GitHub repository settings. Specifically, the username is stored as a variable named DOCKER_USERNAME, and the access token is stored as a secret named DOCKER_PASSWORD. These credentials are then referenced in the workflow file, typically within a step that uses the docker/login-action to authenticate with Docker Hub before the build and push steps execute.

yaml - name: Authenticate to Docker Hub uses: docker/login-action@v3 with: username: ${{ vars.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }}

This authentication step is a prerequisite for any workflow that publishes artifacts to a registry. When combined with multi-stage Dockerfiles, as seen in Node.js applications where a builder stage compiles the application and a release stage creates the final runtime image, the workflow ensures that only the necessary components are included in the final artifact. The RUN --mount=type=cache directive in the Dockerfile can be leveraged during the build process to optimize cache utilization, reducing build times in subsequent runs.

Conclusion

The limitations of native GitHub Actions container support have driven the adoption of third-party actions that offer greater flexibility and control. Tools like pl-strflt/docker-container-action and tonys-code-base/run-container-action enable developers to run arbitrary Docker containers with custom options, bypassing the constraints imposed by the platform's default runners. These actions facilitate more robust CI/CD pipelines, allowing for the use of production-ready images in build steps, complex network configurations, and precise control over runtime arguments. By leveraging these tools, organizations can ensure consistency between development, testing, and production environments, while maintaining the efficiency and automation benefits of GitHub Actions. As containerization continues to evolve, the ability to seamlessly integrate custom Docker executions into workflow pipelines remains a critical capability for modern software engineering.

Sources

  1. Docker Container Action - GitHub Marketplace
  2. Using Docker Run Inside of GitHub Actions - Andrew Schmelyun
  3. Run Docker Container - GitHub Marketplace
  4. Guides - Docker Documentation

Related Posts