Docker Hub, ECR, and Quay aren’t just places to store container images; they’re the central nervous system for distributing your application’s building blocks.
Let’s watch a container image make its journey.
First, we build an image locally. Imagine we have a simple Go application:
package main
import "fmt"
func main() {
fmt.Println("Hello from my container!")
}
We create a Dockerfile:
FROM golang:1.21-alpine
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["/app/myapp"]
Now, we build it:
docker build -t my-awesome-app:v1.0 .
This my-awesome-app:v1.0 image now lives on your local machine. To share it, we need to push it to a registry. Let’s pick Docker Hub first. You’ll need to authenticate:
docker login
Enter your Docker Hub username and password. Then, tag your image with your Docker Hub username and the repository name:
docker tag my-awesome-app:v1.0 your-dockerhub-username/my-awesome-app:v1.0
And push it:
docker push your-dockerhub-username/my-awesome-app:v1.0
Now, anyone with access can pull this image to run your application:
docker pull your-dockerhub-username/my-awesome-app:v1.0
docker run your-dockerhub-username/my-awesome-app:v1.0
This works identically for Amazon Elastic Container Registry (ECR) and Quay. For ECR, you’d first create a repository in the AWS console or via the AWS CLI. Let’s say you created my-ecr-repo. You’d authenticate to ECR:
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <your-aws-account-id>.dkr.ecr.us-east-1.amazonaws.com
Then tag and push:
docker tag my-awesome-app:v1.0 <your-aws-account-id>.dkr.ecr.us-east-1.amazonaws.com/my-ecr-repo:v1.0
docker push <your-aws-account-id>.dkr.ecr.us-east-1.amazonaws.com/my-ecr-repo:v1.0
For Quay, you’d create a repository in the Quay.io web interface. Authentication is similar:
docker login quay.io
Tag and push:
docker tag my-awesome-app:v1.0 quay.io/your-quay-username/my-awesome-app:v1.0
docker push quay.io/your-quay-username/my-awesome-app:v1.0
The core problem these registries solve is distribution and versioning. Without them, you’d be manually shipping tarballs of your built images, a nightmare for consistency and scalability. They provide a standardized API (the Docker Registry API) that docker pull and docker push understand.
Internally, registries store images as a series of layers. When you build an image, Docker creates new layers. When you push, it uploads these layers. Pulling an image means downloading only the layers it doesn’t already have locally. This layer-based storage is incredibly efficient, especially when multiple images share common base layers.
The key levers you control are the image name, tag, and the registry endpoint. The tag (v1.0 in our example) is crucial for versioning. Using latest is convenient but dangerous in production, as it’s mutable and can lead to unexpected behavior if not managed carefully. For production, always use specific, immutable tags like v1.0.1, commit-sha, or a build number.
Most people don’t realize that each image in a registry is identified by a content-addressable digest, a SHA256 hash of the manifest. While you push and pull using tags, Kubernetes and other orchestrators can be configured to pull images by their digest. This guarantees that you are always pulling the exact same image bytes, preventing subtle bugs caused by a tag being accidentally re-pointed to a different image.
The next step is integrating these registries into your CI/CD pipeline for automated builds and deployments.