Baeldung Pro – Ops – NPI EA (cat = Baeldung on Ops)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

Partner – Orkes – NPI EA (cat=Kubernetes)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

1. Introduction

Private Docker images require users to authenticate when accessing stored images in restricted repositories or registries. Unlike public images that anyone can access, these images are only accessible to users with proper credentials, such as usernames and passwords, tokens, or API keys.

Hence, to use a private Docker image in a GitHub Action workflow, authentication and certain configuration must be set up as a part of the workflow.

In this tutorial, we cover essential techniques for securely accessing private Docker registries within a GitHub Action workflow. All commands are executed on Ubuntu 20.04 with Docker Engine version 28.1.1.

2. Prerequisites

Before implementing private Docker image access in GitHub Actions, we need to have several items handled:

  1. GitHub repository with Actions enabled
  2. access to a private Docker registry (here, we use Docker Hub, GitHub Container Registry, AWS Elastic Container Registry, and Google Artifact Registry)
  3. registry credentials with appropriate permissions
  4. basic understanding of GitHub Actions workflow syntax

Next, we create a sample image to use throughout. Let’s define a container via a Dockerfile using the Alpine base image. It should just print Hello from Baeldung! when run:

$ cat Dockerfile
FROM alpine:latest
CMD ["echo", "Hello from Baeldung!"]

Let’s build the image and call it hello-baeldung with an image tag of 1.0 as the version:

$ docker build -f Dockerfile -t hello-baeldung:1.0 .

Furthermore, we add the image name and tag as the values of the IMAGE and TAG environment variables in the terminal, as they are used consistently over the tutorial:

$ IMAGE=hello-baeldung
$ TAG=1.0
$ LOCAL_IMAGE=$IMAGE:$TAG

Next, we can run the image locally to confirm the desired output:

$ docker run hello-baeldung:1.0 
Hello from Baeldung!

As seen above, the output says Hello from Baeldung! as specified in the Dockerfile.

3. Using the GitHub Container Registry (GHCR)

GitHub Container Registry (GHCR) is GitHub’s managed Docker container registry service that enables seamless storage, management, and distribution of Docker images directly within the GitHub ecosystem.

GHCR stores images under the ghcr.io domain:

ghcr.io/USERNAME/IMAGE:TAG
ghcr.io/ORGANIZATION/IMAGE:TAG

Let’s follow the format when pushing and accessing an image.

3.1. Personal Access Token (PAT)

To push the earlier-created image (hello-baeldung:1.0) to GHCR, we first need to authenticate with a GitHub personal access token.

Let’s head over to the GitHub dashboard and, under Developer Settings, create a personal access token (PAT) with the write:packages and delete:packages scope:

GitHub personal access token creation with write:packages and delete:packages selected

These scopes enable us to upload Docker images to the GitHub Container Registry.

To add the personal access token to the CLI, we first provide the token and then assign it to the GHCR_PAT environment variable:

$ GHCR_PAT=<personal_access_token>

Then, we log in to GHCR using the saved credentials:

$ echo "$GHCR_PAT" | docker login ghcr.io -u <GITHUB_USERNAME> --password-stdin

At this point, we should be logged in.

3.2. Push the Image

After logging in with a PAT, also set the repository name for the image:

$ GHCR_REPO=ghcr.io/$GHCR_USER/$IMAGE

While $GHCR_USER is the GitHub user name, $IMAGE is the container image name (hello-baeldung).

Now we can push this image to the GHCR repository using docker push:

$ docker tag $LOCAL_IMAGE $GHCR_REPO:$TAG
$ docker push $GHCR_REPO:$TAG

Once the push is complete, the image is stored privately in the GitHub container registry.

3.3. Private GHCR Images in GitHub Actions

In the last step, we pushed the hello-baeldung Docker image to the GitHub container registry. Now, let’s define how it can fit into the use case in GitHub Actions.

To use a private GHCR Docker image in GitHub Actions, we need to make use of the personal access token (PAT) we created earlier. The PAT has the read:packages scope, which grants the Action all the permissions to pull images and use them in the workflow.

Let’s demonstrate by creating a workflow that uses the private GHCR image we pushed earlier.

Next, we should create a .github/workflows subdirectory in the project directory:

$ mkdir -p .github/workflows

After that, we add a YAML file named ghcr-private-images with the defined workflow:

$ cat .github/workflows/ghcr-private-images.yaml
on:
  push:
    branches:
      - main

jobs:
  run-ghcr-image:
    runs-on: ubuntu-latest
    steps:
      - name: Login to GitHub Container Registry
        run: echo "${{ secrets.TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

      - name: Pull and Run Image
        run: |
            docker pull ghcr.io/${{ secrets.OWNER }}/${{ secrets.IMAGE }}:${{ secrets.TAG }}
            docker run ghcr.io/${{ secrets.OWNER }}/${{ secrets.IMAGE }}:${{ secrets.TAG }}

This workflow automates pulling and running a private container image from GitHub Container Registry (GHCR).

Critically, we need to configure  thesecrets in the GitHub repository as seen in the workflow:

  • TOKEN: GitHub Personal Access Token
  • OWNER: GitHub username or organization name
  • IMAGE: name of the container image (hello-baeldung)
  • TAG: image tag (1.0 in this case)

So, let’s add these variables to the GitHub secrets via the dashboard:

GitHub repository secrets page showing fields for IMAGE, OWNER, TAG, and TOKEN

The workflow runs when we push code to the main branch:

  1. logs into GHCR using the provided token
  2. pulls the private hello-baeldung image
  3. runs the private hello-baeldung image

So, let’s push this workflow to the repository:

$ git add .
$ git commit -m "push workflow to repository"
$ git push

On the Actions dashboard for the repository in GitHub, we should find the latest successful workflow run titled after the commit message.

Furthermore, we can see the container output in the Pull and Run Image step:

GitHub Actions workflow output showing successful container image pull and execution with 'Hello from Baeldung!' message

The GitHub action workflow’s output shows that it successfully pulled and ran the private image.

4. Using Docker Hub

Docker Hub is a container registry provided by Docker itself to share and store container images.

We need a Docker Hub account for this step so we can push the hello-baeldung image to a private repository and use it in a GitHub workflow.

4.1. Personal Access Token (PAT)

To log into the Docker Hub via the CLI, let’s create a personal access token (PAT) on the Docker Hub dashboard with read, write, and delete permissions:

Docker Hub personal access token creation form with fields for token description, expiration date, and permissions

Next, we save the Docker Hub token and username as environment variables after acquiring the PAT:

$ DOCKERHUB_TOKEN=<dockerhub_pat_token>
$ DOCKERHUB_USER=<dockerhub_username>

After that, we log into Docker Hub using docker login:

$ echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USER" --password-stdin

Thus, we should be authenticated.

4.2. Push the Image

Upon a successful login, we define the Docker Hub repository and store it as an environment variable:

$ DOCKERHUB_REPO=$DOCKERHUB_USER/$IMAGE

Finally, let’s push the image to the repository:

$ docker tag $LOCAL_IMAGE $DOCKERHUB_REPO:$TAG
$ docker push $DOCKERHUB_REPO:$TAG

Once pushed, we navigate to the repository image and make it private:

DockerHub repository visibility settings dialog showing options to make hello-baeldung repository private

At this point, we can go ahead and use this private image in a GitHub Action workflow.

4.3. Private Docker Hub Images in GitHub Actions

To use a private image from Docker Hub in a GitHub action workflow, we must first authenticate with Docker Hub credentials.

Specifically, we use the docker/login-action to log into the Docker Hub repository and complete this step. To that end, we set the Docker Hub username and personal access token (PAT) we created in the last step. While it’s recommended to use a personal access token with fewer permissions, such as read-only for pulling images in production environments, we maintain the current permissions for this demonstration.

Let’s create another workflow YAML file named dockerhub-private-image with its defined workflow for private Docker Hub images:

$ cat .github/workflows/dockerhub-private-image.yaml 
name: Run Private DockerHub Image

on:
  push:
    branches:
      - main

jobs:
  run-dockerhub-image:
    runs-on: ubuntu-latest
    steps:
      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Pull and Run Image
        run: |
          docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.IMAGE }}:${{ secrets.TAG }}
          docker run ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.IMAGE }}:${{ secrets.TAG }}

Now, we set the DOCKERHUB_USERNAME and DOCKERHUB_TOKEN variables in the secrets dashboard on GitHub:

GitHub repository secrets page showing fields for DOCKERHUB_USERNAME and DOCKERHUB_TOKEN

Let’s push the workflow to the GitHub repository and see the Action execute itself:

$ git add .
$ git commit -m "push workflow to repository"
$ git push

In the console, we can identify the latest successful Action via its commit. Now, let’s confirm the steps to see whether we received the correct output:

GitHub Actions workflow output showing successful container image pull and execution with 'Hello from Baeldung!' message

As we can see, the private image stored on Docker Hub was successfully used in the GitHub Action workflow.

5. Using AWS Elastic Container Registry

Amazon Elastic Container Registry (ECR) is a Docker container registry managed by AWS. Similar to other registries, it provides a platform for sharing and storing Docker and Open Container Initiative (OCI) images.

AWS ECR also provides secure environments for images. It uses resource-based permissions with IAM as a means of access control. Because of this, it’s ideal for private image storage.

5.1. Account Setup

To integrate private images from AWS ECR with GitHub Actions, we need an active AWS account with administrative CLI access.

Let’s begin by setting the AWS account ID and desired region as variables:

$ AWS_REGION=<aws_region>
$ AWS_ACCOUNT_ID=<account_id>

Once set, we can log in to AWS ECR with docker login:

$ aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com

After getting authenticated, we can continue with the image push.

5.2. Push the Image

Next, we create the repository to save the image:

$ aws ecr describe-repositories --repository-names "$IMAGE" --region $AWS_REGION >/dev/null 2>&1 || \
aws ecr create-repository --repository-name "$IMAGE" --region $AWS_REGION

As seen above, the repository name is the same as that of the image: hello-baeldung. Let’s add the full ECR repository path to the respective environment variable:

$ ECR_REPO=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE

Finally, we tag and push the image to the ECR private repository:

$ docker tag $LOCAL_IMAGE $ECR_REPO:$TAG
$ docker push $ECR_REPO:$TAG

At this point, the hello-baeldung image is in the private ECR repository.

5.3. Creating IAM for Access Control

As mentioned earlier, we also need an IAM user with the necessary permissions to access or use AWS ECR images.

So, let’s create an IAM user named github-ecr-user:

$   aws iam create-user --user-name github-ecr-user
{
    "User": {
        "Path": "/",
        "UserName": "github-ecr-user",
        "UserId": "AIDA6IF55TNK4XVJHP4KT",
        "Arn": "arn:aws:iam::<account_id>:user/github-ecr-user",
        "CreateDate": "2025-08-04T21:07:23+00:00"
    }
}

Next, we create an access key for github-ecr-user:

$ aws iam create-access-key --user-name github-ecr-user
{
    "AccessKey": {
        "UserName": "github-ecr-user",
        "AccessKeyId": "<ACCESS_KEY_ID>",
        "Status": "Active",
        "SecretAccessKey": "<SECRET_ACCESS_KEY>",
        "CreateDate": "2025-08-04T21:07:56+00:00"
    }
}

We should save the output containing the AccessKeyId and SecretAccessKey, as we need them in the GitHub Action workflow.

After that, we create an IAM policy for ECR access and in a JSON file named ecr-policy within the project directory:

$ cat ecr-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:GetRepositoryPolicy",
                "ecr:DescribeRepositories",
                "ecr:ListImages",
                "ecr:DescribeImages",
                "ecr:BatchGetImage"
            ],
            "Resource": "*"
        }
    ]
}

Then, let’s create the policy in AWS:

$ aws iam create-policy \
    --policy-name ECRReadOnlyAccess \
    --policy-document file://ecr-policy.json

This creates the policy in AWS and names it ECRReadOnlyAccess.

Lastly, we attach the policy to the github-ecr-user:

$ aws iam attach-user-policy \
    --user-name github-ecr-user \
    --policy-arn arn:aws:iam::<ACCOUNT_ID>:policy/ECRReadOnlyAccess

Thus, we successfully created the IAM user with its necessary permissions to access ECR.

5.4. Private ECR Images in GitHub Actions

Let’s define the workflow for the private ECR image.

To demonstrate, we create another YAML file named aws-ecr-private-images:

$ cat .github/workflows/aws-ecr-private-images.yaml 
name: Run Private AWS ECR Image

on:
  push:
    branches:
      - main

jobs:
  run-ecr-image:
    runs-on: ubuntu-latest
    steps:
      # Step 1: Configure AWS credentials
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      # Step 2: Login to Amazon ECR
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      # Step 3: Pull and run the private image
      - name: Pull and Run Image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }}
          IMAGE_TAG: ${{ secrets.TAG }}
        run: |
          docker pull $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker run $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

The workflow is similar to the previous workflows, but in this instance, the container registry we use is AWS ECR. Specifically, this workflow leverages the aws-actions/configure-aws-credentials@v4 Action to authenticate with AWS, then logs into the private ECR registry with the aws-actions/amazon-ecr-login@v2 Action, and then pulls and runs the hello-baeldung image we pushed earlier to the repository.

Therefore, we have to add the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, and ECR_REPOSITORY as secrets to the GitHub repository:

GitHub repository secrets page showing fields for AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION and ECR_REPOSITORY

Again, let’s push the new changes to the repository and observe the GitHub Actions dashboard for the latest workflow run.

Once we see a green check on the workflow, we can confirm that the Pull and Run Image step has the required output:

GitHub Actions workflow output showing successful container image pull and execution with 'Hello from Baeldung!' message

As seen above, the workflow run was successful, and the expected output was obtained.

6. Using Google Artifact Registry

Google Artifact Registry is the Google Cloud-managed package manager and container registry used to store container images and language packages such as Maven, npm, and similar.

6.1. Google Cloud Project Setup and Authentication

For this step, we need an active Google Cloud Project with its administrative CLI.

Let’s begin by configuring Docker with gcloud:

$ gcloud auth configure-docker

Once done, we can create the respective repository and image.

6.2. Push the Image

Upon successful configuration of Docker, we add the Google Project ID and Google Container Registry (GCR) repository to the environment:

$ PROJECT_ID=$(gcloud config get-value project)
$ GCR_REPO=gcr.io/$PROJECT_ID/$IMAGE

Next, we tag and push the Docker image to the registry:

$ docker tag $LOCAL_IMAGE $GCR_REPO:$TAG
$ docker push $GCR_REPO:$TAG

Hence, we’ve pushed the hello-baeldung image to the Google Artifact Registry.

6.3. Service Account Key Creation

In this step, let’s create a service account named github-gcr-user needed to authenticate with GitHub Actions:

$ gcloud iam service-accounts create github-gcr-user \
    --description="Service account for GitHub Actions GCR access" \
    --display-name="GitHub GCR User"

Next, we grant the service account the storageObject.viewer and artifactregistry.reader permissions to access the private GCR image:

$ gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:github-gcr-user@$PROJECT_ID.iam.gserviceaccount.com" \
    --role="roles/storage.objectViewer"
...
$ gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:github-gcr-user@$PROJECT_ID.iam.gserviceaccount.com" \
    --role="roles/artifactregistry.reader"
...

Once granted, we create a service account key and dump it to a file called key.json:

$ gcloud iam service-accounts keys create key.json \
    --iam-account=github-gcr-user@$PROJECT_ID.iam.gserviceaccount.com
$ cat key.json
{
  "type": "service_account",
 ...
}

Then, we encode the key in base64 and save it to a file called key.bs4:

$ base64 -w 0 key.json > key.b64

The base64 version of the service account key prevents inconsistency issues, especially with special characters.

6.4. Private GCR Images in GitHub Actions

Let’s define the workflow that utilizes the private GCR image.

In the project directory, we create a YAML file named gcr-private-image and define the new workflow:

$ cat .github/workflows/gcr-private-image.yaml
name: Run Private GCR Image

on:
  push:
    branches:
      - main

jobs:
  run-gcr-image:
    runs-on: ubuntu-latest
    steps:
      - id: 'auth'
        uses: 'google-github-actions/auth@v1'
        with:
            credentials_json: '${{ secrets.GCP_CREDENTIALS }}'
    
      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v1

      # Step 2: Configure Docker for GCR
      - name: Configure Docker for GCR
        run: |
          gcloud auth configure-docker

      # Step 3: Pull and run the private image
      - name: Pull and Run Image
        env:
          GCR_REGISTRY: gcr.io
          GCP_PROJECT: ${{ secrets.GCP_PROJECT_ID }}
          IMAGE_NAME: ${{ secrets.IMAGE }}
          IMAGE_TAG: ${{ secrets.TAG }}
        run: |
          docker pull $GCR_REGISTRY/$GCP_PROJECT/$IMAGE_NAME:$IMAGE_TAG
          docker run $GCR_REGISTRY/$GCP_PROJECT/$IMAGE_NAME:$IMAGE_TAG

This workflow uses the google-github-actions/auth@v1 and google-github-actions/setup-gcloud@v1 Actions to authenticate and pull the image from the Artifact registry.

In this case, we add the GCP_PROJECT_ID and GCP_CREDENTIALS as repository secrets. Specifically, these are the Google Project ID and the encoded base64 service account key, respectively:

GitHub repository secrets page showing fields for GCP_PROJECT_ID and GCP_CREDENTIALS

Let’s push the new workflow to the remote GitHub repository. We then check the latest run output to confirm the results of the Pull and Run Image step:

GitHub Actions workflow output showing successful container image pull and execution with 'Hello from Baeldung!' message

Hence, we’ve successfully pulled and run the private Docker image stored in Google Artifact Registry in GitHub Actions.

7. Conclusion

In this article, we’ve demonstrated how to securely access and use private container images from GitHub Container Registry, Docker Hub, Elastic Container Registry, and Google Artifact Registry in GitHub Actions. Additionally, we used security best practices such as IAM access, repository secrets, and personal access tokens for authentication.