GitHub Actions can’t directly pull images from private Docker registries because the runner environment doesn’t have the necessary credentials by default.
Let’s see how this works in practice. Imagine you have a private Docker registry at my.private.registry.com and you want to use an image named my-app/backend:latest in your GitHub Actions workflow.
name: Build and Deploy
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Log in to private registry
uses: docker/login-action@v2
with:
registry: my.private.registry.com
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Pull private Docker image
run: docker pull my.private.registry.com/my-app/backend:latest
- name: Build application image
run: docker build -t my-app/frontend:latest .
- name: Push application image
run: docker push my-app/frontend:latest
In this workflow, the docker/login-action is the key. It takes your registry URL, username, and password (stored as GitHub Secrets) and configures the Docker client on the runner to authenticate with that registry. Once logged in, subsequent docker pull or docker push commands targeting that registry will succeed.
The problem this solves is simple: how to grant your automated CI/CD process access to proprietary container images without exposing sensitive credentials directly in your workflow files. Storing credentials as GitHub Secrets is crucial. These secrets are encrypted and only available to your workflow runs.
Internally, docker/login-action essentially runs docker login my.private.registry.com -u <username> -p <password>. This command stores the credentials in a Docker configuration file (~/.docker/config.json) on the runner. All subsequent Docker commands executed in the same job will then use these stored credentials for authentication.
The registry, username, and password inputs to docker/login-action are the primary levers you control. You can also specify logout: true if you want to explicitly log out of a registry at the end of a step or job, which can be a good security practice.
The most surprising thing about this process is how seamlessly Docker client integration works. You don’t need to manage complex certificate handling or custom authentication modules for most common private registries. The docker login command handles the underlying authentication handshake, which typically involves token generation and validation.
Most people are aware of storing credentials as secrets, but they often overlook the fact that the docker/login-action can handle multiple registries within a single workflow. You can call it multiple times with different registry, username, and password values to authenticate to different private registries if your build process requires it.
The next hurdle you’ll likely face is managing image scanning for vulnerabilities within these private images.