Skip to Content
Log In to a Private Registry From CI
Advanced
0 likes
Tutorials

Log In to a Private Registry From CI

Log in to a password-protected registry the way a CI job should, push an image, log out, and see the GitHub Actions version. After this, CI logins stop leaking tokens.

A CI job has to log in to your registry on every run, and it is the place where tokens leak most often: in the job log, in a committed config file, or in a script someone copied. This page sets up a local registry with a password, logs in the way a CI job should, and shows the GitHub Actions version. Run on Docker Desktop 4.85.0 (Engine 29.6.2) with registry:2. The user name and password are made up for this demo. The GitHub Actions workflow at the end was not run.

A registry with a password, for testing

New-Item -ItemType Directory -Force ci-login | Out-Null
Set-Location ci-login
New-Item -ItemType Directory -Force auth | Out-Null
docker run --rm --entrypoint htpasswd httpd:2.4-alpine -Bbn ci-bot demo-only-password | Set-Content -Encoding ascii auth\htpasswd
docker run -d --name ci-registry -p 127.0.0.1:5056:5000 -v "${PWD}\auth:/auth:ro" -e REGISTRY_AUTH=htpasswd -e REGISTRY_AUTH_HTPASSWD_REALM=ci -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd registry:2 | Out-Null
Start-Sleep -Seconds 2
"registry up"
registry up

Log in the way CI should

Three rules: the secret comes from an environment variable that the CI fills from its secret store, it goes through standard input, and the job uses its own Docker config folder so nothing lands in a shared profile.

$env:DOCKER_CONFIG = "$PWD\.docker-ci"
New-Item -ItemType Directory -Force $env:DOCKER_CONFIG | Out-Null
$env:CI_REGISTRY_TOKEN | docker login 127.0.0.1:5056 -u ci-bot --password-stdin
docker tag alpine:3.24 127.0.0.1:5056/ci-team/base:3.24
docker push -q 127.0.0.1:5056/ci-team/base:3.24
Get-Content "$env:DOCKER_CONFIG\config.json"
Login Succeeded
127.0.0.1:5056/ci-team/base:3.24

 Info -> Not all multiplatform-content is present and only the available single-platform image was pushed
         sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 -> sha256:d56c381f961d307a21b3ca004cf1e3910f106644aefb1f43e654c8a56c4fd395
{
	"auths": {
		"127.0.0.1:5056": {}
	},
	"credsStore": "wincred"
}

In this demo I set CI_REGISTRY_TOKEN in the terminal before running the commands, to stand in for the CI secret store. In a real job you never type it.

Log out at the end

On a self-hosted runner, the machine stays after the job. Log out, and delete the job's config folder:

docker logout 127.0.0.1:5056
Remove-Item -Recurse -Force $env:DOCKER_CONFIG
Remove-Item Env:DOCKER_CONFIG, Env:CI_REGISTRY_TOKEN
Removing login credentials for 127.0.0.1:5056

On Windows, Docker picked the Windows credential helper by itself: "credsStore": "wincred" appeared in the new config.json, the password went to Windows Credential Manager, and docker logout removed it from there. A Linux runner usually has no credential helper. There the login is written into config.json in that folder, as Base64 that anyone can decode, and Docker prints a warning about it. That is why the folder is per job and deleted at the end.

GitHub Actions

This is the same flow with Docker's login action. I did not run it. The registry address and the variable and secret names are placeholders. Create the token in your registry with the smallest scope that works, for example push and pull to one repository.

name: build
on:
  push:
    branches: [main]
jobs:
  image:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: registry.example.com
          username: ${{ vars.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_TOKEN }}
      - run: docker build -t registry.example.com/team/app:${{ github.sha }} .
      - run: docker push registry.example.com/team/app:${{ github.sha }}

For other CI systems, the same line works in any shell step: echo "$REGISTRY_TOKEN" | docker login registry.example.com -u "$REGISTRY_USER" --password-stdin.

If login fails

  • unauthorized: wrong user or token, or the token has expired or lacks the scope. Check the token's permissions in the registry.
  • http: server gave HTTP response to HTTPS client: the registry runs plain HTTP on an address other than 127.0.0.1. Put HTTPS in front of it. Don't add it to "insecure registries" on CI machines.
  • The token shows up in the job log: something printed it, often a set -x or a debug step. Revoke the token right away and create a new one.

⚠ Warning: Never pass the token with -p on the command line. It ends up in shell history and, on Linux, in the process list that other users can read.

Related course: Docker Advanced.

About

Log in to a password-protected registry the way a CI job should, push an image, log out, and see the GitHub Actions version. After this, CI logins stop leaking tokens.

Responsible
John G
Last Update
08/05/2026
Reading Time
4 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.