Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 91

Automating Docker Image Builds and Publishing with CI/CD

Recommended resources

Hello! Welcome back to our journey into Continuous Integration and Deployment.

In our last lesson, you built a foundational CI pipeline using GitHub Actions. That pipeline automatically compiles your Spring Boot application and runs your tests every time you push code, ensuring your main branch is always in a healthy state. This is a huge step towards professional-grade software development.

However, the artifact produced—the .jar file—is currently lost when the pipeline finishes. To make our application distributable and ready for deployment, we need to package it in a standard, portable format. This is where Docker comes in.

Today, we will address the learning outcome: Extend the CI pipeline to automatically build and publish a Docker image to a registry. We'll modify the workflow you created to take the packaged application, build a Docker image using your Dockerfile, and push it to Docker Hub, a public container registry.

GitHub Actions CI Pipeline for Docker Image Publishing
This diagram illustrates our goal for today: extending our CI pipeline to build a Docker image from our application and push it to a registry like Docker Hub.

1. The Missing Piece: Secure Authentication

To push an image to Docker Hub (or any container registry), our GitHub Actions workflow needs to log in on our behalf. Hardcoding your username and password directly into the workflow file is a major security vulnerability, as this file is part of your code repository.

The correct way to handle this is by using GitHub Secrets. Secrets are encrypted environment variables that you create in your repository's settings. Your workflow can then securely access them without exposing their values in logs or the code itself.

To prepare for this, you will need two things:

  1. A Docker Hub account.
  2. A Personal Access Token from Docker Hub.

Using an access token is more secure than using your password because you can create multiple tokens with specific permissions and revoke them at any time without affecting your main account password.

The following video provides an excellent walkthrough of the entire process we'll be undertaking today. We will focus on specific parts of it to guide our work.

GitHub Actions Tutorial - Basic Concepts and CI/CD Pipeline with Docker

Let's watch a segment from 'GitHub Actions Tutorial' by TechWorld with Nana. This part will show you how to securely log in to Docker Hub and push an image from a GitHub Actions workflow.

Watch the video from 25:15 to 29:30. Pay close attention to these key steps: Using an Action: The video uses a pre-built action to handle Docker login, build, and push operations. Using Secrets: Notice how the workflow file references secrets.DOCKERHUB_USERNAME and secrets.DOCKERHUB_PASSWORD. (Note: We will use a token for the password). Creating Secrets: The video demonstrates where to go in your repository's Settings to create these secrets (Settings > Secrets and variables > Actions). For now, just watch to understand the process. We will create the secrets and modify the file together in the next section.

Now, let's get your credentials ready:

  1. If you don't have one, create a free account on Docker Hub.
  2. Once logged in, navigate to your Account Settings by clicking on your profile icon.
  3. Go to the Security tab and click "New Access Token".
  4. Give your token a descriptive name (e.g., github-actions-token), set its permissions to "Read & Write", and click "Generate".
  5. Important: Docker Hub will only show you this token once. Copy it immediately and store it somewhere safe temporarily.

Next, add your credentials to your GitHub repository's secrets:

  1. In your GitHub repository, go to Settings > Secrets and variables > Actions.
  2. Click "New repository secret".
  3. Create a secret named DOCKERHUB_USERNAME and paste your Docker Hub username as the value.
  4. Create another secret named DOCKERHUB_TOKEN and paste the access token you just generated as the value.

You are now set up to authenticate securely from your workflow!


2. Extending the CI Workflow

Let's modify the main.yml file from the previous lesson. We'll add new steps to the end of our existing build job. These steps will log in to Docker Hub and then build and push the image.

We will use a set of official, community-supported actions provided by Docker, which make this process straightforward and reliable.

Step-by-Step Guide to Spring Boot CI/CD Using GitHub Actions

The article 'Step-by-Step Guide to Spring Boot CI/CD Using GitHub Actions' provides excellent, production-ready examples of the actions we'll be using. We'll use this as a reference for our code.

Read the subsections 'Authenticate to Docker Hub', 'Set Up Docker Buildx', and 'Build and Push the Image'. Focus on the YAML snippets and the explanation for each action (docker/login-action, docker/setup-buildx-action, and docker/build-push-action). This will give you a clear picture of what we're about to add to our workflow.

Based on these best practices, let's update your .github/workflows/main.yml.

Here is the complete workflow file. The new steps are added at the end of the steps list.

name: Java CI with Maven

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    name: Build, Test, and Publish
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up JDK 17 and cache Maven packages
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'
        cache: maven

    - name: Build with Maven
      run: mvn -B package --file pom.xml

    # --- New steps start here ---

    - name: Login to Docker Hub
      uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Build and push Docker image
      uses: docker/build-push-action@v6
      with:
        context: .
        push: true
        tags: ${{ secrets.DOCKERHUB_USERNAME }}/your-repo-name:latest

Let's break down the new steps:

  1. Login to Docker Hub: This step uses the docker/login-action. It retrieves the username and token you stored in GitHub Secrets and uses them to authenticate with Docker Hub. The ${{ secrets.VARIABLE_NAME }} syntax is how you securely access secrets.

  2. Set up Docker Buildx: This step sets up Buildx, a Docker CLI plugin that enables advanced build features. While not strictly necessary for a simple build, it's a best practice that enables optimizations like build caching and multi-platform builds in the future.

  3. Build and push Docker image: This is the main event. The docker/build-push-action does two things:

    • It runs docker build, using your project's Dockerfile (the context: . tells it to look in the root directory).
    • Because push: true is set, it then pushes the built image to the registry.
    • The tags property specifies how the image should be named in the registry. You must replace your-repo-name with the desired name for your image repository on Docker Hub (e.g., my-spring-app).
Test your understanding!

Imagine you want to tag each image with both the latest tag and a unique tag based on the Git commit SHA (a unique identifier for each commit). How might you modify the tags property?

Hint: GitHub Actions provides a context variable github.sha that holds the commit SHA.

Show answer

You would provide a multi-line string for the tags property, with each tag on a new line:

      with:
        context: .
        push: true
        tags: |
          ${{ secrets.DOCKERHUB_USERNAME }}/your-repo-name:latest
          ${{ secrets.DOCKERHUB_USERNAME }}/your-repo-name:${{ github.sha }}

This is a common and powerful pattern for versioning your images.


3. Execution and Verification

Now, it's time to see your enhanced pipeline in action.

  1. Update your local .github/workflows/main.yml with the complete code from the section above. Remember to replace your-repo-name in the tags property.

  2. Commit the changes and push them to your main branch.

    git add .github/workflows/main.yml
    git commit -m "ci: Add steps to build and publish Docker image"
    git push origin main
    
  3. Navigate to the Actions tab in your GitHub repository. You will see your new workflow run being triggered.

  4. Click on the workflow run and watch the steps execute. You should see the new "Login to Docker Hub" and "Build and push Docker image" steps. If everything is configured correctly, they will complete with green checkmarks.

  5. Once the workflow succeeds, go to your Docker Hub profile. In your repositories list, you should see the new repository (your-repo-name) containing the image with the latest tag.

Congratulations! Your CI pipeline is now a CI/CD-ready pipeline. It doesn't just validate your code; it produces a versioned, distributable artifact that is ready to be deployed anywhere.


Conclusion

Today you've made a significant leap forward in automating your development process. By extending your CI pipeline, you've connected your source code repository directly to a container registry, laying the groundwork for automated deployments.

Key Takeaways:

  • Always use GitHub Secrets to store sensitive credentials like API tokens; never hardcode them.
  • Docker Hub Access Tokens provide a more secure way to grant CI/CD systems access than using your primary password.
  • Specialized GitHub Actions (docker/login-action, docker/build-push-action) simplify complex tasks and make your workflows cleaner and more maintainable.
  • A complete CI pipeline produces a distributable artifact, which in modern development is often a version-tagged Docker image in a registry.

What's Next?

Our pipeline now automatically builds, tests, and publishes our application as a container image. The final piece of the puzzle is deploying it. In the next lesson, Implement a CD pipeline to automatically deploy the containerized application to a cloud platform, we will add a final job to our workflow. This job will be triggered upon a successful image push and will instruct a cloud service to pull our new image and run it, completing our end-to-end automation from code commit to a live application.