NoVPS
PricingFAQDocumentationBlog
Sign InSign Up
Tutorials

Docker pull image from private registry: auth & tags

Kate Baker

Fri, Apr 17, 2026

Main picture

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

NoVPS

NoVPS 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 NoVPS 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 NoVPS handle this differently. You push your Docker image to NoVPS'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.

Be first in line for updates
and special pricing

Get early access to new features and exclusive discounts delivered straight to your inbox

Legal

Privacy PolicyTerms and ConditionsAcceptable Use Policy
NoVPS

© 2026 NoVPS Cloud LTD

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.