Docker pull image from private registry: auth & tags

By Kate BakerLast updated
Docker pull image from private registry: auth & tags

Pulling a public image from Docker Hub is one command. Pulling from a private registry introduces authentication, tag management, credential storage, and platform-specific quirks that trip up even experienced developers.

This guide covers everything involved in pulling images from private registries — from the initial docker login to handling authentication in CI, managing tags and digests, and troubleshooting the errors you'll inevitably encounter.

How docker pull image works under the hood

When you run docker pull, Docker doesn't download a single file. It fetches an image manifest — a JSON document that lists every layer (compressed filesystem diff) that makes up the image. Docker then downloads each layer individually, skipping any it already has locally.

docker pull nginx:1.25

This command does the following:

  1. Resolves the registry — nginx with no prefix defaults to docker.io/library/nginx
  2. Fetches the manifest for tag 1.25
  3. Downloads each layer referenced in the manifest
  4. Assembles them into a usable local image

For private registries, there's an additional step before all of this: authentication. Docker must present valid credentials to the registry's API before it will serve the manifest or any layers.

Authenticating with docker login

The basic authentication command works with any OCI-compliant registry:

docker login registry.example.com

Docker prompts for a username and password, validates them against the registry, and stores the credentials locally. After that, any docker pull image command targeting that registry uses the stored credentials automatically.

# After login, pulling just works
docker pull registry.example.com/myteam/api-server:latest

Where credentials are stored

By default, Docker stores credentials in ~/.docker/config.json:

{
  "auths": {
    "registry.example.com": {
      "auth": "dXNlcm5hbWU6cGFzc3dvcmQ="
    }
  }
}

That auth value is a base64-encoded username:password. It is not encrypted. Anyone with read access to this file has your registry credentials.

The default ~/.docker/config.json stores credentials in base64, not encryption. On shared machines, CI runners, or anywhere multiple users have access, use a credential helper instead.

Credential helpers for secure storage

Docker supports credential helpers that delegate storage to your OS keychain or a cloud provider's credential manager:

# macOS — uses Keychain (installed with Docker Desktop)
"credHelpers": {
  "registry.example.com": "osxkeychain"
}

# Linux — uses pass or secretservice
sudo apt install golang-docker-credential-helpers
# Then in ~/.docker/config.json:
"credHelpers": {
  "registry.example.com": "secretservice"
}

For cloud registries, each provider has its own helper:

RegistryCredential helperInstall
AWS ECRdocker-credential-ecr-loginbrew install docker-credential-helper-ecr
GCP Artifact Registrydocker-credential-gcloudIncluded with gcloud CLI
Azure ACRdocker-credential-acr-envaz acr login handles it

Pulling from major private registries

Each cloud registry has its own authentication flow. Here's how to docker pull image from each one.

AWS ECR

ECR uses temporary tokens that expire every 12 hours:

# Get a temporary auth token and login
aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com

# Now pull
docker pull 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:v2.1.0

The --password-stdin flag avoids passing the password as a command argument, which would be visible in process listings and shell history.

Common pitfall: forgetting that ECR tokens expire. If your pull fails with 401 Unauthorized after working earlier in the day, re-run the login command.

Google Artifact Registry

# Authenticate with gcloud
gcloud auth configure-docker us-docker.pkg.dev

# Pull
docker pull us-docker.pkg.dev/my-project/my-repo/myapp:v2.1.0

The gcloud auth configure-docker command writes a credential helper config to ~/.docker/config.json that uses your active gcloud session automatically.

Azure ACR

# Login
az acr login --name myregistry

# Pull
docker pull myregistry.azurecr.io/myapp:v2.1.0

GitHub Container Registry (GHCR)

# Use a personal access token (classic) with read:packages scope
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin

# Pull
docker pull ghcr.io/myorg/myapp:v2.1.0

Granite

Granite provides a built-in container registry for every project. Generate an access token in the dashboard — it gives you a username and password pair for Docker authentication:

# Login using the access token credentials from the Granite dashboard
docker login nl.registry.novps.app

# Pull
docker pull nl.registry.novps.app/namespace/repository:latest

No extra setup needed — the registry is part of the platform, so pulling and deploying use the same credentials.

Self-hosted registries

If you're running your own registry (Docker Registry, Harbor, GitLab Registry), the login is straightforward:

docker login registry.yourcompany.com
docker pull registry.yourcompany.com/team/service:latest

For registries behind a VPN or on a private network, make sure your Docker daemon can reach the registry host. If it uses a self-signed TLS certificate, you'll need to add it to Docker's trusted certificates:

# Copy the CA cert to Docker's certificate directory
sudo mkdir -p /etc/docker/certs.d/registry.yourcompany.com
sudo cp ca.crt /etc/docker/certs.d/registry.yourcompany.com/ca.crt

# Restart Docker
sudo systemctl restart docker

Understanding tags, digests, and what you're actually pulling

When you docker pull image by tag, you're pulling whatever that tag currently points to. Tags are mutable — the maintainer can push a completely different image under the same tag at any time.

Tags

docker pull myregistry.com/myapp:v2.1.0
docker pull myregistry.com/myapp:latest
docker pull myregistry.com/myapp:main-abc1234

The :latest tag is not special. It's not "the newest version" — it's just a tag name that Docker uses as a default when you don't specify one. Many teams push to :latest with every build, but some don't use it at all.

Digests

A digest is an immutable, content-addressed identifier. It guarantees you get exactly the same image every time:

docker pull myregistry.com/myapp@sha256:a1b2c3d4e5f6...

Use digests when:

  • Deploying to production and you need reproducibility
  • Auditing or compliance requires you to prove exactly which image is running
  • You want to guarantee that a docker pull image on two different machines produces identical results

Use tags when:

  • Developing locally and you want the latest build
  • Running staging environments that should auto-update
  • You don't need strict reproducibility

Listing available tags

Docker itself doesn't have a built-in command to list remote tags. You can use the registry API directly:

# Docker Hub
curl -s "https://hub.docker.com/v2/repositories/library/nginx/tags?page_size=20" | \
  python3 -m json.tool

# Any OCI-compliant registry (with auth)
curl -s -u username:password \
  "https://registry.example.com/v2/myapp/tags/list" | \
  python3 -m json.tool

Or use tools like skopeo or crane for a cleaner experience:

# Using crane
crane ls registry.example.com/myapp

# Using skopeo
skopeo list-tags docker://registry.example.com/myapp

Pulling images in CI/CD pipelines

CI environments need non-interactive authentication. Never put credentials in your pipeline configuration files — use your CI platform's secrets management.

GitHub Actions

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Login to private registry
        uses: docker/login-action@v3
        with:
          registry: registry.example.com
          username: ${{ secrets.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_TOKEN }}

      - name: Pull and deploy
        run: |
          docker pull registry.example.com/myapp:${{ github.sha }}

GitLab CI

deploy:
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - echo "$REGISTRY_PASSWORD" | docker login registry.example.com -u "$REGISTRY_USER" --password-stdin
  script:
    - docker pull registry.example.com/myapp:$CI_COMMIT_SHORT_SHA

Key security practices for CI

  • Use short-lived tokens where possible (ECR's 12-hour tokens, GCP's service account keys with expiration)
  • Scope credentials to read-only when the pipeline only needs to pull, not push
  • Rotate secrets regularly — set a calendar reminder if your CI platform doesn't enforce rotation
  • Never log credentials — watch out for debug modes that dump environment variables to stdout

Troubleshooting common pull failures

denied: access forbidden or 401 Unauthorized

Your credentials are missing, expired, or lack the required permissions.

# Verify you're logged in
cat ~/.docker/config.json | python3 -m json.tool

# Re-authenticate
docker logout registry.example.com
docker login registry.example.com

If using ECR, remember that tokens expire every 12 hours. If using a service account, verify it has pull or read permissions on the repository.

manifest unknown or tag not found

The tag you're requesting doesn't exist. This usually means:

  • The image was never pushed with that tag
  • The tag was deleted or overwritten
  • You have a typo in the repository path or tag name
# Check if the repository exists and list available tags
crane ls registry.example.com/myapp

net/http: TLS handshake timeout or connection errors

Network issue between your machine and the registry. Check:

  • DNS resolution: nslookup registry.example.com
  • Firewall rules: registries typically use port 443
  • VPN: are you connected to the right network?
  • Proxy: does Docker need an HTTP proxy configured?
# Configure Docker proxy if needed
# In ~/.docker/config.json or /etc/docker/daemon.json
{
  "proxies": {
    "default": {
      "httpProxy": "http://proxy.company.com:8080",
      "httpsProxy": "http://proxy.company.com:8080",
      "noProxy": "localhost,127.0.0.1"
    }
  }
}

Slow pulls: image is too large

If pulling takes minutes, the image might be bloated. Check the size before pulling:

crane manifest registry.example.com/myapp:latest | python3 -m json.tool
# Look at the "size" field for each layer

Common causes of oversized images:

  • Using a full OS base image (ubuntu:22.04) instead of a minimal one (alpine or distroless)
  • Including build tools, source code, or test dependencies in the final image
  • Not using multi-stage builds

Pulling images for deployment

Once you can reliably docker pull image from your private registry, the next question is what to do with it. On a single server, you run docker run. Across multiple servers, you need orchestration — Kubernetes, Docker Swarm, or a managed platform.

If orchestration sounds like more infrastructure than you want to manage, platforms like Granite handle this differently. You push your Docker image to Granite's built-in container registry, and the platform runs it on managed infrastructure — no need to configure pull credentials on production servers, set up registry mirrors, or manage secrets rotation across a cluster. For teams that want Docker's packaging model without the operational work around registries and deployment, it's a more streamlined path.

Quick reference: docker pull command patterns

# Pull from Docker Hub (public)
docker pull nginx:1.25

# Pull from Docker Hub (private)
docker login
docker pull myorg/myapp:latest

# Pull from a private registry
docker login registry.example.com
docker pull registry.example.com/myapp:v2.1.0

# Pull by digest (immutable)
docker pull registry.example.com/myapp@sha256:abc123...

# Pull a specific platform (e.g., ARM on an x86 machine)
docker pull --platform linux/arm64 myapp:latest

# Pull all tags (rarely needed, can be very slow)
docker pull --all-tags myapp

# Pull without running — just download the image
docker pull registry.example.com/myapp:latest

Summary

Pulling images from private registries comes down to three things: authenticating correctly, referencing the right tag or digest, and keeping credentials secure. Use docker login with --password-stdin for interactive use, credential helpers for local development, and CI-native secrets management for pipelines. Prefer digests over tags when reproducibility matters. And if pull failures have you stuck, check credentials first, then network, then verify the tag actually exists.

Keep reading

More posts tagged “Tutorials”.
GHCR vs Granite: Docker registry or managed deploys?

GHCR is a great place to store your Docker images — but it doesn't run them. This guide breaks down what GitHub Container Registry actually does, where teams hit the wall trying to deploy on top of it, and how to decide between rolling your own infrastructure, pairing GHCR with a managed platform, or skipping the registry question entirely. Includes a concrete GitHub Actions walkthrough and an honest take on the trade-offs.

Mon, May 11, 2026
How to download Dockerfile from any Docker Hub image

You pulled an image from Docker Hub, it works, and now you want to see the Dockerfile behind it. Maybe you need to customize it, audit it for security, or just understand how the image was built. The problem is that Docker Hub doesn't store Dockerfiles — it stores images. And images are not Dockerfiles.

Fri, Apr 10, 2026
Fix docker start container errors: causes and solutions 2026

You run `docker start container`, and nothing happens. Or worse — it exits immediately with a cryptic error you've never seen before. The container worked yesterday. Now it doesn't. This guide covers every major reason a container fails to start and gives you concrete steps to fix each one.

Mon, Apr 13, 2026
How to serve build locally with Docker Compose up

A practical guide to serving a static frontend build locally using Docker Compose, covering everything from a simple Nginx volume mount to full-stack setups with proxying, environment variables, and common troubleshooting fixes.

Tue, Mar 24, 2026
Docker vs Kubernetes: when Docker Compose is enough

A solo-founder decision framework for choosing between Docker Compose, Kubernetes, and managed platforms. Includes a five-step decision tree, signals that you've outgrown Compose, and common mistakes early-stage teams make.

Mon, May 4, 2026
How to start docker daemon on Linux, Mac & Windows

You typed  docker ps , hit enter, and got something like this: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? Or maybe the Windows variant:  error during connect: this error may indicate that the docker daemon is not running. Read this article to find out how to fix these issues.

Fri, May 8, 2026
Be first in line for updates
and special pricing
Get early access to new features and exclusive discounts delivered straight to your inbox