DevOps.Academy
CoursesCurriculumPricing

    Loading…

    ↑ ↓ move · ↵ open · Ctrl K anywhere

    ◔My learningSign in
    DevOps.Academy

    Free, hands-on DevOps lessons. Made with care in India.

    Learn

    All coursesLinux roadmapCurriculumMy learning

    Academy

    PricingCertificates

    Legal

    PrivacyTermsContact

    devops.rajeev.pro

    DockerLesson 8 of 14
    Your progress · 0%
    On this page
    1. What's in a name
    2. Tags move, digests don't
    3. Docker Hub
    4. GHCR and ECR
    5. Tagging best practices
    ↖ Course roadmap
    Lesson 8 · Registries

    Registries: Docker Hub, GHCR, ECR and tags you can trust

    Share images through a registry: name and push them, log in to Docker Hub, GHCR and ECR, and tag so every deploy is traceable. Tags can move; a digest never does, and this lesson proves it.

    30 min read · intermediate · hands-on

    By the end of this lesson you'll be able to

    0 of 5 complete

    What's in a name

    An image lives in your local Docker until you push it to a registry, a server that stores images so other machines can pull them. The full name says where:

    ghcr.io / acme/price-api : 1.4.0  @sha256:034074b2…
    registry   repository     tag      digest
    

    Leave out the registry and Docker assumes Docker Hub: nginx:1.30-alpine is really docker.io/library/nginx:1.30-alpine. To push, you tag the image with the registry's name, then push. Try it with a registry running on your own machine:

    $ docker run -d --name registry -p 127.0.0.1:5000:5000 registry:3
    $ docker tag price-api:multi localhost:5000/price-api:1.0.0
    $ docker push localhost:5000/price-api:1.0.0
    96ed2737ae31: Pushed
    a5789fc40e82: Pushed
    b8b68e3af840: Pushed
    1.0.0: digest: sha256:034074b20fb92e756118038e8c32abeb0c269da12b78451263de0b83cdef6ccd size: 856
    $ docker tag price-api:multi localhost:5000/price-api:sha-3f9c2e1
    $ docker push localhost:5000/price-api:sha-3f9c2e1
    3214acf345c0: Layer already exists
    7c12895b777b: Layer already exists
    sha-3f9c2e1: digest: sha256:034074b20fb92e756118038e8c32abeb0c269da12b78451263de0b83cdef6ccd size: 856
    $ curl -s localhost:5000/v2/price-api/tags/list
    {"name":"price-api","tags":["1.0.0","sha-3f9c2e1"]}
    
    docker run -d --name registry -p 127.0.0.1:5000:5000 registry:3
    • dockerbuild and run containers
    • runstart a new container
    • -drun in the background
    • --name registryname the container
    • -p 127.0.0.1:5000:5000publish a port, host:container
    • registry:3the image to start
    docker tag price-api:multi localhost:5000/price-api:1.0.0
    • taggive an image another name
    • price-api:multithe image, then its new name
    • localhost:5000/price-api:1.0.0the image, then its new name
    docker push localhost:5000/price-api:1.0.0
    • pushupload an image to a registry
    • localhost:5000/price-api:1.0.0the image
    docker tag price-api:multi localhost:5000/price-api:sha-3f9c2e1
    • localhost:5000/price-api:sha-3f9c2e1the image, then its new name
    docker push localhost:5000/price-api:sha-3f9c2e1
    • localhost:5000/price-api:sha-3f9c2e1the image
    curl -s localhost:5000/v2/price-api/tags/list
    • curlmake an HTTP request
    • -ssilent: no progress bar

    The second push uploaded nothing: layers are stored by their hash, and the registry already had them. Two tags, one digest.

    Tags move, digests don't

    A tag is a name that points at an image. A digest is the SHA-256 hash of the image's manifest: change one byte of content and the digest changes. Watch what happens when a different build is pushed as 1.0.0:

    Same tag, different content, no warning. Only the digest is a promise.© Animation: DevOps Academy

    Same tag, different content, no warning. Only the digest is a promise.© Animation: DevOps Academy
    $ docker tag price-api:single localhost:5000/price-api:1.0.0
    $ docker push localhost:5000/price-api:1.0.0
    1.0.0: digest: sha256:367778fc7a085fc1131a5d04d26bb8ffb0e48d45bde2b5aa67bbd4bae48c7881 size: 856
    $ docker pull localhost:5000/price-api@sha256:034074b20fb92e756118038e8c32abeb0c269da12b78451263de0b83cdef6ccd
    Status: Downloaded newer image for localhost:5000/price-api@sha256:034074b2…
    
    docker tag price-api:single localhost:5000/price-api:1.0.0
    • dockerbuild and run containers
    • taggive an image another name
    • price-api:singlethe image, then its new name
    • localhost:5000/price-api:1.0.0the image, then its new name
    docker push localhost:5000/price-api:1.0.0
    • pushupload an image to a registry
    • localhost:5000/price-api:1.0.0the image
    docker pull localhost:5000/price-api@sha256:034074b20fb92e756118038e8c32abeb0c269da12b78451263de0b83cdef6ccd
    • pulldownload an image
    • localhost:5000/price-api@sha256:034074b20fb92e756118038e8c32abeb0c269da12b78451263de0b83cdef6ccdthe image

    The registry accepted the overwrite without a word. A server that pulled 1.0.0 yesterday and one that pulls it today now run different code. The digest still fetches the original. Find an image's digest with docker buildx imagetools inspect localhost:5000/price-api:sha-3f9c2e1 or in the push output.

    Docker Hub

    Docker Hub is the default registry and home of the official images. Create a free account, then:

    $ docker login -u yourname                    # prompts for a personal access token
    $ docker tag price-api:multi yourname/price-api:1.0.0
    $ docker push yourname/price-api:1.0.0
    
    docker login -u yourname
    • dockerbuild and run containers
    • loginsign in to a registry
    docker tag price-api:multi yourname/price-api:1.0.0
    • taggive an image another name
    • price-api:multithe image, then its new name
    • yourname/price-api:1.0.0the image, then its new name
    docker push yourname/price-api:1.0.0
    • pushupload an image to a registry
    • yourname/price-api:1.0.0the image

    Use a personal access token from your account settings, not your password, and give it only the scope it needs (read-only for servers that just pull). Docker Hub limits how many images can be pulled per hour, with lower limits for anonymous pulls, so log in on CI runners and servers too; the current limits are in the usage and limits page linked below. Free accounts can keep public repositories, plus a limited number of private ones.

    GHCR and ECR

    Most teams keep images next to their code or their cloud. GitHub Container Registry (ghcr.io) uses your GitHub login and permissions:

    $ echo "$GHCR_TOKEN" | docker login ghcr.io -u yourname --password-stdin
    $ docker tag price-api:multi ghcr.io/yourname/price-api:1.0.0
    $ docker push ghcr.io/yourname/price-api:1.0.0
    
    echo "$GHCR_TOKEN" | docker login ghcr.io -u yourname --password-stdin
    • echoprint text
    • |pipe: send output to the next command
    • dockerbuild and run containers
    • loginsign in to a registry
    docker tag price-api:multi ghcr.io/yourname/price-api:1.0.0
    • taggive an image another name
    • price-api:multithe image, then its new name
    • ghcr.io/yourname/price-api:1.0.0the image, then its new name
    docker push ghcr.io/yourname/price-api:1.0.0
    • pushupload an image to a registry
    • ghcr.io/yourname/price-api:1.0.0the image

    In GitHub Actions, the job's own GITHUB_TOKEN can push, given permissions: packages: write (the CI/CD course builds this workflow). New packages are private until you change their visibility.

    Amazon ECR gives each AWS account a registry, logged in to with a 12-hour token from the AWS CLI:

    $ aws ecr create-repository --repository-name price-api --image-tag-mutability IMMUTABLE
    $ aws ecr get-login-password --region eu-west-1 \
        | docker login --username AWS --password-stdin 123456789012.dkr.ecr.eu-west-1.amazonaws.com
    $ docker tag price-api:multi 123456789012.dkr.ecr.eu-west-1.amazonaws.com/price-api:sha-3f9c2e1
    $ docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/price-api:sha-3f9c2e1
    
    aws ecr create-repository --repository-name price-api --image-tag-mutability IMMUTABLE
    • awsthe AWS command line
    aws ecr get-login-password --region eu-west-1 \
    • --regionwhich Region
    docker tag price-api:multi 123456789012.dkr.ecr.eu-west-1.amazonaws.com/price-api:sha-3f9c2e1
    • dockerbuild and run containers
    • taggive an image another name
    • price-api:multithe image, then its new name
    • 123456789012.dkr.ecr.eu-west-1.amazonaws.com/price-api:sha-3f9c2e1the image, then its new name
    docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/price-api:sha-3f9c2e1
    • pushupload an image to a registry
    • 123456789012.dkr.ecr.eu-west-1.amazonaws.com/price-api:sha-3f9c2e1the image

    IMMUTABLE makes ECR reject a push to an existing tag, which turns the overwrite you saw above into an error. In CI, get AWS credentials through OIDC instead of stored keys (CI/CD course, lesson on secrets).

    Never docker login -p

    docker login -p secret leaves the password in your shell history and in the process list. Pipe it in with --password-stdin. docker login saves credentials in ~/.docker/config.json, only base64-encoded unless a credential helper (the OS keychain, pass) is set up: never commit or copy that file.

    Tagging best practices

    TagUse
    sha-3f9c2e1 (the git commit)every build: says exactly which code is inside, never reused
    1.4.0, plus 1.4 and 1releases; the short ones move on purpose to the newest patch
    main, pr-42the latest build of a branch or pull request, for test environments
    latestconvenience only: never deploy it, never pin it

    Deploy by digest, or by the SHA tag, never by a tag that moves. Then you can always say what's running, and a rollback means pointing back at an earlier digest that is still in the registry. Set a cleanup policy too: registries keep every untagged layer until you delete it, and storage costs money.

    In production

    Push from CI only, with a token that can write to one repository. Make release tags immutable where the registry supports it, keep images for as long as you might roll back to them, and record the digest you deployed alongside the release.

    ⌘
    Mini mission

    Prove a tag can lie

    Run a private registry on your machine, push one image under a version tag and a commit tag, then push a different image under the same version tag. Show the digests change, and pull the original back by digest.

    Input: Docker and the two price-api images from lesson 7Output: Two different digests for one tag, and a successful pull by the original digest
    Reveal one safe solution
    $ docker run -d --name registry -p 127.0.0.1:5000:5000 registry:3
    $ docker tag price-api:multi localhost:5000/price-api:1.0.0
    $ docker push localhost:5000/price-api:1.0.0 | tail -1              # note the digest
    $ docker tag price-api:multi localhost:5000/price-api:sha-3f9c2e1
    $ docker push localhost:5000/price-api:sha-3f9c2e1 | grep -c "already exists"
    $ docker tag price-api:single localhost:5000/price-api:1.0.0
    $ docker push localhost:5000/price-api:1.0.0 | tail -1              # a different digest
    $ docker pull localhost:5000/price-api@sha256:<the first digest>
    $ docker rm -f registry
    
    docker run -d --name registry -p 127.0.0.1:5000:5000 registry:3
    • dockerbuild and run containers
    • runstart a new container
    • -drun in the background
    • --name registryname the container
    • -p 127.0.0.1:5000:5000publish a port, host:container
    • registry:3the image to start
    docker tag price-api:multi localhost:5000/price-api:1.0.0
    • taggive an image another name
    • price-api:multithe image, then its new name
    • localhost:5000/price-api:1.0.0the image, then its new name
    docker push localhost:5000/price-api:1.0.0 | tail -1
    • pushupload an image to a registry
    • localhost:5000/price-api:1.0.0the image
    • |pipe: send output to the next command
    • tailshow the last lines
    docker tag price-api:multi localhost:5000/price-api:sha-3f9c2e1
    • localhost:5000/price-api:sha-3f9c2e1the image, then its new name
    docker push localhost:5000/price-api:sha-3f9c2e1 | grep -c "already exists"
    • localhost:5000/price-api:sha-3f9c2e1the image
    • grepfind lines that match a pattern
    • -ccount matching lines
    docker tag price-api:single localhost:5000/price-api:1.0.0
    • price-api:singlethe image, then its new name
    docker pull localhost:5000/price-api@sha256:<the first digest>
    • pulldownload an image
    • localhost:5000/price-api@sha256:<thethe image
    • firstthe image
    • digest>the image
    docker rm -f registry
    • rmdelete a container
    • -fstop it first if running
    • registrythe container

    Same tag, two digests: the first push's content is still there, reachable only by its digest or by the sha-3f9c2e1 tag. That's why deploys pin one of those.

    Try it yourself

    0 of 6 steps done

    Run each task in your own account or terminal, then tick it off.

    Knowledge check

    Quick check

    Question 1 of 5

    In ghcr.io/acme/price-api:1.4.0, what is ghcr.io?

    Go deeper

    Official reading for this lesson

    • DocsDocker Docs: docker image pushDocker Docs ↗
    • DocsDocker Docs: Docker Hub usage and limitsDocker Docs ↗
    • DocsGitHub Docs: working with the Container registryGitHub Docs ↗
    • DocsAWS Docs: pushing a Docker image to Amazon ECRAWS Docs ↗
    • ToolDistribution registryCNCF Distribution ↗

    Or get every quiz answer right and it completes itself.

    ← PreviousBuilding images: Dockerfiles, the layer cache and multi-stage buildsNext lesson →Running containers well: restart policies, limits, config and Compose