
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.
Last updated: September 10, 2025
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.
Before implementing private Docker image access in GitHub Actions, we need to have several items handled:
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.
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.
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:
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.
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.
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:
So, let’s add these variables to the GitHub secrets via the dashboard:
The workflow runs when we push code to the main branch:
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:
The GitHub action workflow’s output shows that it successfully pulled and ran the private image.
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.
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:
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.
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:
At this point, we can go ahead and use this private image in a GitHub Action workflow.
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:
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:
As we can see, the private image stored on Docker Hub was successfully used in the GitHub Action workflow.
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.
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.
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.
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.
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:
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:
As seen above, the workflow run was successful, and the expected output was obtained.
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.
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.
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.
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.
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:
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:
Hence, we’ve successfully pulled and run the private Docker image stored in Google Artifact Registry in GitHub Actions.
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.