How to Automate Docker Image Building and Publishing with Pack CLI and GitHub Actions

In modern software development, efficiently building and publishing Docker images is crucial for streamlining deployment processes. Traditionally, this involved writing complex Dockerfiles and managing dependencies manually. However, with the introduction of the Pack CLI tool, which leverages Cloud Native Buildpacks, creating container images directly from application source code has become much simpler.

In this tutorial, we‘ll walk through a step-by-step GitHub workflow that automates building and publishing a Docker image for a sample application to Docker Hub using the powerful Pack CLI. By the end, you‘ll have a reusable template for setting up CI/CD pipelines to automatically package and distribute your own applications as Docker images.

What is Pack CLI?

Pack CLI is a tool that utilizes Cloud Native Buildpacks to transform application source code into runnable container images, without needing to write Dockerfiles. Buildpacks are pluggable, modular tools that encapsulate the knowledge of how to compile and package applications for specific language ecosystems and frameworks.

Essentially, buildpacks detect and provide the necessary dependencies, runtimes, and configuration to build an optimized container image for your application. Pack CLI makes using buildpacks easy by providing a simple, declarative interface on top of the low-level buildpacks API.

Prerequisites

Before diving into the workflow, ensure you have the following prerequisites:

  • Docker: Although optional for this workflow since building happens remotely, having Docker installed locally is recommended for testing and debugging purposes.

  • Pack CLI: To build images locally and inspect the output, install the Pack CLI following the official documentation for your platform.

  • Docker Hub Account: You‘ll need a Docker Hub account and authentication token to publish the built images. Sign up for an account if you don‘t already have one.

  • GitHub Account: The example workflow will be triggered by actions in a GitHub repository. Log in or create a new account to follow along.

To use the sample application, clone the repository at https://github.com/yourusername/sample-app.git. You can also adapt the workflow for your own application by modifying the relevant configurations.

Creating the GitHub Workflow

Now let‘s break down the GitHub workflow step-by-step. Create a new file named build-and-publish.yml in the .github/workflows directory of your repository.

1. Triggering the Workflow

First, define the events that trigger the workflow to run:

on:
  push:
    branches: 
      - main
  pull_request:
    branches:
      - main

This configuration runs the workflow whenever changes are pushed or a pull request is made to the main branch. Adjust the branch name if using a different branch for production builds.

2. Environment Variables

Next, specify environment variables used throughout the workflow:

env:
  BUILDER: "heroku/builder"
  IMG_NAME: "sample-app" 
  USERNAME: "${{ secrets.DOCKER_USERNAME }}"

Here, BUILDER defines the buildpack builder image to use. We‘re using the heroku/builder for demonstration, but you can find other builders or even create your own. IMG_NAME sets a name for the final Docker image that will be published. USERNAME references your Docker Hub account name via a repository secret, which we‘ll configure in a later step.

3. Checking Out the Repository

To access the application source code, the workflow needs to check out the repository:

jobs:
  build-and-publish:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

This job named build-and-publish runs on an Ubuntu virtual machine and uses the actions/checkout action to clone the repository code.

4. Setting the Image Name

To avoid naming collisions, prefix the image name with your Docker Hub username:

    - name: Set image name
      run: echo "IMG_NAME=${{ env.USERNAME }}/${{ env.IMG_NAME }}" >> $GITHUB_ENV  

This step appends the USERNAME secret to the IMG_NAME and sets it as an environment variable.

5. Authenticating with Docker Hub

Before the workflow can publish images, it needs to log in to Docker Hub:

    - name: Log in to Docker Hub
      uses: docker/login-action@v1
      with:
        username: ${{ env.USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}  

Use the docker/login-action to authenticate with the Docker registry. The username comes from the USERNAME environment variable set earlier, and the password references a DOCKER_PASSWORD secret.

To set up the secrets, go to your repository settings, navigate to "Secrets", and click "New repository secret". Create secrets named DOCKER_USERNAME and DOCKER_PASSWORD with your Docker Hub credentials.

6. Building and Publishing the Docker Image

With authentication handled, the workflow can build the application into a Docker image and publish it to Docker Hub:

    - name: Build and publish
      uses: dfreilich/[email protected]
      with:
        args: ‘build ${{ env.IMG_NAME }} --builder ${{ env.BUILDER }} --publish‘

This step utilizes the dfreilich/pack-action to run Pack CLI commands in the workflow. The args parameter specifies the command to execute, which builds an image using the configured BUILDER and IMG_NAME, and publishes it with the --publish flag.

Pack CLI abstracts away the process of analyzing the application, selecting the appropriate buildpack, and constructing the final Docker image. The buildpack detects the language and framework, vendors dependencies, and sets up the necessary runtime and configuration, all without a Dockerfile.

7. Testing the Application

To ensure the application built and runs correctly, start a container and make a test request:

    - name: Test application
      run: |
        docker run -d --name sample-app -p 8080:8080 ${{ env.IMG_NAME }}
        sleep 10s
        curl -s http://localhost:8080 | grep "Hello, World!"

First, start a detached container named sample-app from the published image, mapping port 8080. After a 10-second sleep to allow startup, make a request using curl and grep to check for the expected "Hello, World!" response. If the expected text is missing, the step fails.

8. Rebasing the Image

To optimize the image for reproducibility and upgrades, use Pack CLI‘s rebase command:

    - name: Rebase image
      uses: dfreilich/[email protected]
      with: 
        args: ‘rebase ${{ env.IMG_NAME }}‘

Rebasing rebuilds the image on top of the latest version of the underlying base image and buildpacks, without needing to fully rebuild the application layers. This ensures the latest OS patches, security fixes, and buildpack improvements are incorporated.

9. Inspecting the Final Image

Finally, inspect the published image metadata and bill of materials:

    - name: Inspect image
      uses: dfreilich/[email protected]
      with:
        args: ‘inspect-image ${{ env.IMG_NAME }}‘

The inspect-image command provides details about the image‘s buildpacks, dependencies, and configuration, which is useful for auditing and debugging.

The Complete Workflow

Putting it all together, here‘s the complete workflow YAML file:

name: Build and Publish Image

on:
  push:
    branches: 
      - main
  pull_request:
    branches:
      - main

env:
  BUILDER: "heroku/builder" 
  IMG_NAME: "sample-app"
  USERNAME: "${{ secrets.DOCKER_USERNAME }}"

jobs:
  build-and-publish:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

    - name: Set image name
      run: echo "IMG_NAME=${{ env.USERNAME }}/${{ env.IMG_NAME }}" >> $GITHUB_ENV

    - name: Log in to Docker Hub 
      uses: docker/login-action@v1
      with:
        username: ${{ env.USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}

    - name: Build and publish
      uses: dfreilich/[email protected]
      with:
        args: ‘build ${{ env.IMG_NAME }} --builder ${{ env.BUILDER }} --publish‘

    - name: Test application
      run: |
        docker run -d --name sample-app -p 8080:8080 ${{ env.IMG_NAME }}
        sleep 10s
        curl -s http://localhost:8080 | grep "Hello, World!"

    - name: Rebase image 
      uses: dfreilich/[email protected]
      with:
        args: ‘rebase ${{ env.IMG_NAME }}‘

    - name: Inspect image
      uses: dfreilich/[email protected]
      with:
        args: ‘inspect-image ${{ env.IMG_NAME }}‘

With this workflow in place, every push or pull request to the main branch triggers an automated build and publish of the application as an optimized Docker image to Docker Hub. The workflow also runs tests and inspects the final image to ensure it‘s properly built.

Conclusion

By leveraging Cloud Native Buildpacks via the Pack CLI, we‘ve created a simple yet powerful GitHub workflow to automate building and publishing Docker images. This approach simplifies the containerization process by abstracting away low-level configuration and focusing on the application source code.

Some key benefits of this workflow include:

  • Eliminating the need to write and maintain complex Dockerfiles
  • Automatically detecting and optimizing the application runtime and dependencies
  • Generating efficient, reproducible, and secure container images
  • Enabling rapid iteration and deployment of applications
  • Providing a consistent and standardized build process across projects and teams

The example workflow in this tutorial provides a solid foundation you can easily adapt and extend for your own applications and pipelines. Integrate additional testing, vulnerability scanning, or deployment steps to further enhance your CI/CD process.

By adopting an automated and standardized approach to building and publishing container images, you can boost productivity, reliability, and consistency in your software development lifecycle. Happy building!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *