Pushing an image to a private registry comes down to three commands, but the one that trips people up is docker tag. Docker decides where to send an image purely from the registry prefix baked into its name, so a myapp:latest with no prefix goes to Docker Hub no matter how carefully you authenticated against AWS a moment earlier. Get the prefix right and the push just works. Get it wrong and you'll get cryptic denied errors even though you're certain you logged into the correct registry.
The sequence below works for any private registry. Authentication is the part that differs by provider, so after the basics there are specifics for Amazon Elastic Container Registry (ECR), Google Artifact Registry, and Azure Container Registry, plus the Docker Hub rate limit that quietly breaks CI pipelines on free accounts.
The three commands
Here's the complete flow, assuming you've already built an image tagged myapp:1.4.0 locally. First, authenticate to the registry:
1docker login registry.example.com
You'll be prompted for a username and password, and on success you'll see:
1Login Succeeded
Next, retag the local image so its name includes the registry hostname and your repository path:
1docker tag myapp:1.4.0 registry.example.com/team/myapp:1.4.0
This doesn't copy or rebuild anything. It adds a second name pointing at the same image ID, and that name is what tells Docker which registry to target. Now push it:
1docker push registry.example.com/team/myapp:1.4.0
1234The push refers to repository [registry.example.com/team/myapp]5f70bf18a086: Pusheda3b5c2d1e4f6: Pushed1.4.0: digest: sha256:9c4e... size: 1572
Each line is a layer being uploaded. Layers already present in the registry are skipped and reported as Layer already exists, which is why your second push of a similar image is usually much faster than the first.
If you skip the tag step and run docker push myapp:1.4.0 directly, Docker expands that to docker.io/library/myapp:1.4.0 and tries to push to Docker Hub's official-images namespace, which you don't have access to. The fix is always to include the full registry path in the tag.
Authenticating to Amazon ECR
ECR doesn't use a static password. You request a short-lived token from the AWS CLI and pipe it into docker login. Your IAM principal needs the ecr:GetAuthorizationToken permission for this to work:
123aws ecr get-login-password --region us-east-1 \| docker login --username AWS \--password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
The username is always the literal string AWS, never your IAM username. The token lasts 12 hours, so in CI you re-run this at the start of each job rather than caching credentials. Once authenticated, tag and push against the registry URI:
123docker tag myapp:1.4.0 \123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:1.4.0docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:1.4.0
One catch specific to ECR: the repository must already exist before you push. ECR won't create it on the fly the way Docker Hub does. Run aws ecr create-repository --repository-name myapp first, or you'll get a name unknown error. If you're still using the old aws ecr get-login command, drop it. It was deprecated years ago in favor of get-login-password.
Authenticating to Google Artifact Registry
If you're reaching for Google Container Registry and gcr.io, stop. Container Registry was shut down on March 18, 2025, and reads stopped working after June 3, 2025. Artifact Registry is the successor, and it uses *-docker.pkg.dev hostnames. Existing gcr.io URLs now resolve to Artifact Registry-backed repositories, but new work should target pkg.dev directly.
Rather than a one-shot login, the gcloud CLI installs a credential helper that Docker calls automatically. You configure it once per region:
1gcloud auth configure-docker us-central1-docker.pkg.dev
12Adding credentials for: us-central1-docker.pkg.devDocker configuration file updated.
After that, Docker fetches a fresh access token from your gcloud session on every push, and the token refreshes itself roughly every hour with no further action from you. The repository path includes your project ID and repository name:
1234docker tag myapp:1.4.0 \us-central1-docker.pkg.dev/my-project/my-repo/myapp:1.4.0docker push \us-central1-docker.pkg.dev/my-project/my-repo/myapp:1.4.0
If you add repositories in other regions later, run gcloud auth configure-docker again with those hostnames, because the helper only handles the regions you've explicitly registered.
Authenticating to Azure Container Registry
ACR ties into your existing Azure CLI session. Sign in to Azure, then authenticate to the registry by its short name (no domain suffix):
1az acr login --name myregistry
1Login Succeeded
Behind the scenes this sets a Microsoft Entra token in your Docker config, and that token lasts 3 hours. Push using the full azurecr.io login server name in lowercase:
12docker tag myapp:1.4.0 myregistry.azurecr.io/myapp:1.4.0docker push myregistry.azurecr.io/myapp:1.4.0
az acr login shells out to the Docker daemon, so it needs Docker running locally. In environments without the daemon, such as Azure Cloud Shell or a credential-only CI step, use az acr login --name myregistry --expose-token to get a raw token you can feed into docker login separately.
The Docker Hub rate limit that breaks free accounts
The gotcha that catches teams off guard comes from pulls, not pushes. Docker Hub enforces pull limits of 100 pulls per 6 hours for unauthenticated users (counted per IPv4 address or IPv6 /64 subnet) and 200 pulls per 6 hours for authenticated Docker Personal (free) accounts. Pro, Team, and Business plans get unlimited pulls under a fair use policy. Docker announced stricter hourly limits — 10 pulls per hour for unauthenticated users and 100 per hour for free accounts — for April 1, 2025, but never enforced them; what actually took effect that date was unlimited pulls for paid subscribers.
In a CI pipeline this adds up fast. Every build pulls a base image, every deploy pulls the image you just pushed, and a shared CI runner pools all of that under one IP. A multi-architecture image counts as a separate pull per architecture, so a single docker pull of an amd64 plus arm64 manifest burns two of your allotment. When you hit the ceiling, pulls fail with toomanyrequests: You have reached your pull rate limit, and the build that passed yesterday breaks today with nothing in your code changed. Authenticating your CI runner with docker login lifts you from the 100-per-6-hours unauthenticated tier to the 200-per-6-hours Personal tier, which is often enough to stop the bleeding.
There's a second, quieter limit on the push side. A free Docker Hub account allows only one private repository. Push a second private image and Docker rejects it until you delete the first or upgrade. Teams discover this the moment they add a second service, so if you're pushing private images to Docker Hub, plan on a paid plan or a different registry from the start.
Final thoughts
You now have the core push sequence and the authentication path for each major cloud registry: a 12-hour token for ECR, a self-refreshing credential helper for Artifact Registry, and a 3-hour session token for ACR. The two things worth remembering are that the registry prefix in the image tag is what routes the push, and that Docker Hub's free-account pull limits will surface in CI long before you ever notice them locally.
Once your images are running in production, the registry is only the starting line. Container logs are ephemeral by default, resource usage is invisible without instrumentation, and a misbehaving container in a fleet is hard to find. Dash0's infrastructure monitoring tracks container and pod resource usage alongside real-time logs and distributed traces, so you can follow a deployed image from registry to running workload in one view.
Start a free trial to monitor your containers, pods, and clusters in one place. No credit card required.