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!