Created
December 8, 2025 13:15
-
-
Save brianjbayer/cd8fb8398ec901cca471841f66498da8 to your computer and use it in GitHub Desktop.
Revisions
-
brianjbayer created this gist
Dec 8, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,575 @@ # Developing and Reusing Dockerfile-based Actions in GitHub Actions  --- GitHub Actions is powerful tool for automating workflows within the GitHub ecosystem such as Continuous Integration/Continuos Deployment (CI/CD). One of its standout features is the ability to create custom `action`s that can be tailored to perform specific tasks. Among the different types of `action`s, `Dockerfile`-based `action`s provide a flexible and scalable approach to customize your workflows using your base image and language of choice. In this post, we'll explore how to develop and reuse a `Dockerfile`-based GitHub Actions `action`, allowing you to encapsulate your logic in a Docker container. By leveraging Docker, we can ensure consistency across different environments, make the action portable, and reuse it in multiple workflows. Whether you're creating a simple automation script or a complex CI/CD pipeline, `Dockerfile`-based `action`s can help you standardize and streamline your development process. --- ## Creating a Dockerfile-based Action To create `Dockerfile`-based `action` you need at least three things... 1. A subdirectory for your `action` (e.g. `./.github/workflows/actions/delete-docker-hub-repository`) 2. A `Dockerfile` located in your `action` subdirectory that runs your task as an entrypoint 3. An `action.yml` file containing the `actions` `input`s including `secret`s and any `output`s GitHub Actions `Dockerfile`-based `action`s run by the convention of simply referencing the `action`'s subdirectory and supplying any inputs and GitHub Actions will build the `Dockerfile` and run it, running your custom task in the container. This leads to the **fourth thing** you will almost certainly need, your **custom script** for your task. This is what males the Dockerfile `action` so powerful and useful, because you supply the executing code, you can *write it in any language* and use any base image that *you like*. However, `Dockerfile`-based `action` build and run the image every time they are called, so smaller and simpler images are preferred. Finally the **fifth thing** you should include is a `README.md` for your `action` documenting what it does and how to use it and test it. And because your `action` is Docker based, you can use containerized development with nothing more than Docker and your favorite editor or IDE. While this is an example of a `Dockerfile`-based `action` that uses a Ruby script to delete a list of Docker Hub image repositories, it shows you the general approach to developing these types of `action`s: 1. Create the subdirectory for your `action` 2. Create the `Dockerfile` and containerized development environment 3. Develop your custom script for your task 4. Create the `actions.yml` ### Create the Subdirectory for Your Action In your GitHub repository, create the subdirectory for your `action`, for example... ```bash mkdir -p ./.github/workflows/actions/delete-docker-hub-repository ``` ### Create the `Dockerfile` and Containerized Development Environment You just need the name of your custom script at this point as the `Dockerfile` `action` pattern is a simple `Dockerfile` that copies your script into the image and runs it as the entrypoint. 1. Create the file for your custom script in the `action`'s subdirectory, for example... ```bash touch ./.github/workflows/actions/delete-docker-hub-repository/delete-docker-hub-repository.rb ``` 2. Create your `Dockerfile` in the `action`'s subdirectory that copies and runs your custom script as the entrypoint, for example... ```dockerfile #--- Base Image --- ARG BASE_IMAGE=ruby:3.4.6-slim-trixie FROM ${BASE_IMAGE} AS ruby-base #--- Run Action Stage --- # Make the entrypoint script executable COPY --chmod=0755 delete-docker-hub-repository.rb /delete-docker-hub-repository.rb # Set the entrypoint to the script ENTRYPOINT ["/delete-docker-hub-repository.rb"] ``` > :bulb: The examples and references usually have an `alpine` > base image, but a debian-based `slim` is not that > much bigger, is more robust, and runs `bash` by default > :gear: `COPY --chmod=0755` eliminates the added layer to `RUN` > a separate `chmod` command to make your script executable #### Build and Run Your Image as a Containerized Development Environment You can now use your `Dockerfile` to develop your script. 1. Change to your `action`'s subdirectory, for example... ```bash cd ./.github/workflows/actions/delete-docker-hub-repository ``` 2. Build your image (even though it does not really have a script yet), for example... ``` docker build --no-cache -t delete-docker-hub-repository . ``` 3. Run a containerized development environment for developing your script by running your image interactively and volume-mounting your working directory (you will need to override the entrypoint which is your script) for example... ``` docker run --rm -it -v $(pwd):/app -w /app --entrypoint 'bash' delete-docker-hub-repository ``` Your entrypoint working directory is at the container root directory i.e. `/`, but you can not volume mount over it, so you need to volume mount to a different directory and set it as the working directory > :bulb: if using an `alpine`-based image, be sure to use `sh` as your > entrypoint command ### Develop Your Custom Script For Your Action Develop your script in the `action`'s subdirectory and run and test it in the running container. * Use environment variables for your `input`s * Write to the special GitHub Actions `$GITHUB_OUTPUT` environment file for `output`s #### Inputs Using Environment Variable The `action` in this example will have three `input`s. For example, using environment variables in a Ruby script for these inputs... ```ruby docker_hub_username = ENV['DOCKER_HUB_USERNAME'] docker_hub_password = ENV['DOCKER_HUB_PASSWORD'] docker_hub_repository_name = ENV['DOCKER_HUB_REPOSITORY'] ``` The `action.yml` file will map each declared input to its corresponding environment variable, making them available inside the container and your script at runtime. #### Example Action Ruby Script Here is a full example of a custom `action` script written in Ruby that deletes a Docker Hub image repository. <details> <summary>Example Ruby Script for Docker Hub Repository Deletion</summary> ```ruby #!/usr/bin/env ruby # frozen_string_literal: true require 'net/http' require 'uri' require 'json' SUCCESS_STATUS_CODE = 200 ACCEPTED_STATUS_CODE = 202 NOT_FOUND_STATUS_CODE = 404 docker_hub_username = ENV['DOCKER_HUB_USERNAME'] docker_hub_password = ENV['DOCKER_HUB_PASSWORD'] docker_hub_repository_name = ENV['DOCKER_HUB_REPOSITORY'] if docker_hub_username.nil? || docker_hub_password.nil? || docker_hub_repository_name.nil? warn 'Missing necessary environment variables!' exit 1 end # Docker Hub login API endpoint login_url = URI('https://hub.docker.com/v2/users/login/') login_payload = { username: docker_hub_username, password: docker_hub_password } # Get the authentication token uri = URI.parse(login_url.to_s) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json' }) request.body = login_payload.to_json response = http.request(request) if response.code.to_i == SUCCESS_STATUS_CODE response_body = JSON.parse(response.body) auth_token = response_body['token'] warn 'Successfully authenticated' else warn "Error authenticating: #{response.body}" exit 2 end # Check if the repository exists check_url = URI("https://hub.docker.com/v2/repositories/#{docker_hub_username}/#{docker_hub_repository_name}/") warn "Checking existence of repository: #{check_url}" # GET the repository uri = URI.parse(check_url.to_s) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Get.new(uri.path) # Send the request to Docker Hub API check_response = http.request(request) check_response_code = check_response.code.to_i if check_response_code == SUCCESS_STATUS_CODE warn "#{check_response_code}: Repository '#{docker_hub_repository_name}' exists" elsif check_response_code == NOT_FOUND_STATUS_CODE warn "#{check_response_code}: Error Repository '#{docker_hub_repository_name}' not found" exit 2 else warn "#{check_response_code}: Error checking repository: #{check_response.body}" exit 2 end # Delete the repository delete_url = URI("https://hub.docker.com/v2/repositories/#{docker_hub_username}/#{docker_hub_repository_name}/") warn "Deleting: #{delete_url}" # Create DELETE request to remove the repository uri = URI.parse(delete_url.to_s) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Delete.new(uri.path, { 'Authorization' => "JWT #{auth_token}" }) # Send the request to Docker Hub API delete_response = http.request(request) delete_response_code = delete_response.code.to_i delete_response_body = delete_response.body if delete_response_code == ACCEPTED_STATUS_CODE warn "#{delete_response_code}: Repository '#{docker_hub_repository_name}' has been deleted successfully" else warn "#{delete_response_code}: Failed to delete repository: #{delete_response_body}" exit 2 end ``` </details> #### Outputs For any outputs, your script needs to *write* to the special GitHub Actions environment variable `$GITHUB_OUTPUT`. Here are some examples... Writing to the `action` output `my-output` in `bash`... ```bash my_output='something useful' # Write to the GitHub Actions output echo "my-output=$my_output" >> $GITHUB_OUTPUT ``` Or writing to the `action` output `my-output` in Ruby... ```ruby my_output = 'something useful' # Write to the GitHub Actions output File.open(ENV['GITHUB_OUTPUT'], 'w') do |file| file.puts "my-output=#{my_output}" # You can replace key=value with the actual key-value pair end ``` ### Create your `action.yml` File Now that you have your `action`'s `Dockerfile` and custom script, you can create the `action.yml` metadata file to connect your `Dockerfile` and custom script to GitHub Actions. Here you define your inputs, linking them to your environment variables in your custom script, your outputs, and that this is a `Dockerfile`-based `action`. For example, this is the `action.yml` for the `action` to delete a DockerHub image repository... ```yaml name: Delete Docker Hub Repository description: "Deletes the specified Docker Hub Repository" inputs: docker-hub-repository: description: "Docker Hub Username" required: true docker-hub-username: description: "Docker Hub Username" required: true docker-hub-password: description: "Docker Hub Password" required: true runs: using: docker image: Dockerfile env: DOCKER_HUB_REPOSITORY: ${{ inputs.docker-hub-repository }} DOCKER_HUB_USERNAME: ${{ inputs.docker-hub-username }} DOCKER_HUB_PASSWORD: ${{ inputs.docker-hub-password }} ``` The Action's `input`s which correspond to the environment variables in the custom script are defined in the block... ```yaml inputs: docker-hub-repository: description: "Docker Hub Username" required: true docker-hub-username: description: "Docker Hub Username" required: true docker-hub-password: description: "Docker Hub Password" required: true ``` :eyes: Secrets such as the `docker-hub-username` and `docker-hub-password` are passed as `action` inputs, BUT are recognized and handled securely as secrets by GitHub Actions. Any outputs would be defined similarly at this `yaml` level... ```yaml outputs: my-output: # id of output description: 'Something awesome' ``` The `runs` block defines your `action` as a `Dockerfile`-based `action`... ```yaml runs: using: docker image: Dockerfile ``` This is also where you pass the `action` inputs as environment variables to the running container which contains your custom script... ```yaml runs: ... env: DOCKER_HUB_REPOSITORY: ${{ inputs.docker-hub-repository }} DOCKER_HUB_USERNAME: ${{ inputs.docker-hub-username }} DOCKER_HUB_PASSWORD: ${{ inputs.docker-hub-password }} ``` --- ## Using Your Dockerfile-Based Action Now that you have created your `action`, you can use it in your Github Actions workflows. However, this topic is where the [Docker documentation](https://docs.github.com/en/actions/tutorials/use-containerized-services/create-a-docker-container-action#example-using-a-public-action) (and AI responses) are lacking and incomplete. ### Git Checkout Your Action This may be obvious, but since your `action` is in "code" instead of a simple workflow YAML (configuration) file, you must `git checkout` the repository (and reference) that contains your `action`. :bulb: This `git checkout` is required **even if you are calling your`action` from the same repository** it is in. For example, this actual workflow job calling an `action` in the same repository (`uses: ./.github/workflows/actions/delete-docker-hub-repository`)... ```yaml jobs: test-delete-docker-hub-repository-action: name: Test Delete Docker Hub Repository Action runs-on: ubuntu-latest steps: - name: Delete Docker Repos uses: ./.github/workflows/actions/delete-docker-hub-repository with: docker-hub-repository: "sample-login-watir-cucumber_add-parallel" docker-hub-username: ${{ secrets.DOCKER_HUB_USERNAME }} docker-hub-password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} ``` Produces this error... ``` Error: Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '/home/runner/work/actions-image-cicd/actions-image-cicd/.github/workflows/actions/delete-docker-hub-repository'. Did you forget to run actions/checkout before running your local action? ``` #### Git Checkout Your Local Action To use your `action` from the same repository, add a checkout `step` to the `job` calling your local `action`... ```yaml - uses: actions/checkout@v6 ``` > :sparkles: It is common practice to use the official `action` > [`actions/checkout`](https://github.com/actions/checkout) > to checkout your repository in GitHub Actions For example to fix, the failing workflow above... ```yaml jobs: test-delete-docker-hub-repository-action: name: Test Delete Docker Hub Repository Action runs-on: ubuntu-latest steps: # Checkout this repository with the action to be called - uses: actions/checkout@v6 - name: Delete Docker Repos uses: ./.github/workflows/actions/delete-docker-hub-repository with: docker-hub-repository: "sample-login-watir-cucumber_add-parallel" docker-hub-username: ${{ secrets.DOCKER_HUB_USERNAME }} docker-hub-password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} ``` #### Git Checkout Your Actions From Another Repository If you want to use your `Dockerfile`-based `action` in another repository making it truly reusable, you must also add a `git checkout` step in your calling `job`, but you must... * Explicitly specify the repository with your `action` * :closed_lock_with_key: if your `action` is in a private repository, you must also add authorization > :sparkles: It is common practice to use the offical `action` > [`actions/create-github-app-token`](https://github.com/actions/create-github-app-token?tab=readme-ov-file#use-app-token-with-actionscheckout) > to generate a one-time access token to checkout your repository > in GitHub Actions * Consider checkout order and location if checking out multiple repositories * :bulb: It is generally a best-fit practice to checkout any workflow code to its own unique subdirectory and after any source repositories are checked out to the workflow runner current directory * Later checkouts will overwrite previous checkouts if not made to a unique subdirectory ### When to Create a Wrapper Workflow For Your Action To make it even easier to reuse your `Dockerfile`-based `action`, you may want to "wrap" it in a [Reusable Workflow](https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows). This allows you to encapsulate the `git checkout` and any other setup and/or any other common processing related to your `action`. However, a *wrapper* workflow is generally not recommended if your `action` is intended to process or needs data or files created in a previous step within the same job in other workflows. These ephemeral artifacts would not be available to the `action` in the wrapper. The example `action` presented here does not require any artifacts from a prior step so a wrapper workflow makes sense. Here is a wrapper Reusable Workflow for the example `action` that deletes a Docker Hub image repository. Tt contains the `git checkout` of the `Dockerfile`-based `action` as well as an example of creating a [*Summary Report*](https://github.blog/news-insights/product-news/supercharging-github-actions-with-job-summaries/)... > :bulb: Note the `ref` `input` which allows you to specify a > version (as a `git` reference) for your called `action` ```yaml name: Delete Image Repositories on: workflow_call: inputs: runner: description: "The type of runner for this workflow (Default: ubuntu-latest)" required: false type: string default: ubuntu-latest repositories: description: "Docker Hub repositories to delete (JSON array)" required: true type: string summary: description: "Add a summary report to the workflow run (Default: true)" required: false type: boolean default: true ref: description: "The git reference for the action" required: false type: string secrets: registry_u: description: The username for the docker login required: true registry_p: description: The password (PAT) for the docker login required: true jobs: delete-image-repository: name: Delete ${{ matrix.repository }} runs-on: ${{ inputs.runner }} strategy: fail-fast: false matrix: repository: ${{ fromJSON(inputs.repositories) }} steps: - name: Checkout action repository uses: actions/checkout@v5 with: repository: brianjbayer/actions-image-cicd ref: ${{ inputs.ref }} path: action-repo # THe Docker Hub API needs just the repository name, not the namespace/org - name: Extract Hub repository id: extract-repo-name run: | repo="${{ matrix.repository }}" hub_repo="${repo##*/}" echo "hub_repo=$hub_repo" >> "$GITHUB_OUTPUT" echo "hub_repo: [$hub_repo]" - name: Delete Hub repository ${{ steps.extract-repo-name.outputs.hub_repo }} id: delete-docker-repos uses: ./action-repo/.github/actions/delete-docker-hub-repository with: docker-hub-repository: ${{ steps.extract-repo-name.outputs.hub_repo }} docker-hub-username: ${{ secrets.registry_u }} docker-hub-password: ${{ secrets.registry_p }} - name: Summary report if: ${{ inputs.summary == true && (success() || failure()) }} run: | if [[ ${{ steps.delete-docker-repos.outcome }} == 'success' ]] ; then message=":white_check_mark: Deleted Docker Hub repository \`${{ matrix.repository }}\`" else message=":x: Failed to delete Docker Hub repository \`${{ matrix.repository }}\`" fi cat <<EOF >> $GITHUB_STEP_SUMMARY ### Docker Hub Repository Deletion Repository: \`${{ matrix.repository }}\` ${message} EOF ``` > :octocat: You can find the example `Dockerfile`-based `action` > at [brianjbayer/actions-image-cicd](https://github.com/brianjbayer/actions-image-cicd/tree/main/.github/actions/delete-docker-hub-repository) > as well as the wrapper > [Reusable workflow](https://github.com/brianjbayer/actions-image-cicd/blob/main/.github/workflows/delete_docker_hub_repositories.yml) ---