Skip to content

Pushing Images to AWS ECR


GHCR worked with zero credentials because GitHub already trusted GitHub. Amazon ECR has no reason to extend you that same trust — it’s a completely separate cloud provider, and this is the chapter where AWS authentication complexity actually enters the pipeline. The image itself doesn’t change at all; only where it ends up does.

Creating the ECR Repository

Unlike GHCR, which quietly created a package the first time you pushed to it, an ECR repository has to exist before anything can be pushed there:

aws ecr create-repository \
  --repository-name devstuffs-hello \
  --region us-east-1 \
  --image-scanning-configuration scanOnPush=true

scanOnPush runs a basic vulnerability scan against every image the moment it lands — not something you’d rely on as your only security check, but it’s free, automatic, and worth turning on by default. The command’s output includes a repositoryUri (something like 123456789012.dkr.ecr.us-east-1.amazonaws.com/devstuffs-hello) — save it, the workflow needs it.

Storing AWS Credentials as Repo Secrets

If you already added AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as repo secrets for the S3/CloudFront chapter earlier in this section, you can reuse them here as long as the IAM user behind them also has ECR permissions (ecr:GetAuthorizationToken, plus ecr:BatchCheckLayerAvailability, ecr:PutImage, ecr:InitiateLayerUpload, ecr:UploadLayerPart, ecr:CompleteLayerUpload scoped to this repository). Otherwise, create a fresh IAM user with just those permissions and add its keys the same way you did before.

Note

As with the S3 and EC2 chapters, this uses long-lived access-key secrets — the straightforward, deliberately naive way most people start. A later chapter in this course shows a way to authenticate to AWS without storing any long-lived keys at all.

The Workflow

This extends the GHCR workflow from the last chapter rather than replacing it — the image gets pushed to both registries in the same run, since there’s no reason to throw away the working GHCR push:

name: Build and Push Docker Image

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  packages: write

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4

      - name: Log in to GHCR
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v6
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1

      - name: Log in to Amazon ECR
        id: ecr-login
        uses: aws-actions/amazon-ecr-login@v1

      - name: Extract image metadata
        id: meta
        uses: docker/metadata-action@v6
        with:
          images: |
            ghcr.io/${{ github.repository_owner }}/devstuffs-hello
            ${{ steps.ecr-login.outputs.registry }}/devstuffs-hello
          tags: |
            type=sha,format=long
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

The moving part worth understanding is steps.ecr-login.outputs.registry. The amazon-ecr-login action authenticates the local Docker daemon against your ECR registry (the same effect as running aws ecr get-login-password and piping it into docker login, just handled for you) and also outputs the registry’s hostname — you don’t hardcode your 12-digit AWS account ID into the workflow anywhere, since that output already has the exact <account-id>.dkr.ecr.<region>.amazonaws.com string baked in.

docker/metadata-action accepting a multi-line images: list is what makes this one build feed two registries: it generates the full matching set of tags for both image names, and build-push-action pushes to every tag in that combined list in a single build — the image is only actually built once, then pushed twice, not built twice.

Verifying the Push

Confirm the image landed in ECR, not just GHCR:

aws ecr describe-images --repository-name devstuffs-hello --region us-east-1

You should see both a sha-<commit> tag and a latest tag listed. Pulling it requires authenticating Docker to ECR locally first, the same way the workflow did:

aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
docker image pull 123456789012.dkr.ecr.us-east-1.amazonaws.com/devstuffs-hello:latest
docker container run -d -p 8080:8080 --name devstuffs-hello-ecr 123456789012.dkr.ecr.us-east-1.amazonaws.com/devstuffs-hello:latest
curl http://localhost:8080
docker container stop devstuffs-hello-ecr
docker container rm devstuffs-hello-ecr

Cleanup

Unlike GHCR on a public repo, ECR storage isn’t free — you’re charged per GB-month for every image sitting in the repository, even one you never pull again. If you’re not moving on to the next chapter right away, delete the images or the whole repository:

aws ecr batch-delete-image \
  --repository-name devstuffs-hello \
  --region us-east-1 \
  --image-ids imageTag=latest imageTag=sha-<commit>

# or, to remove everything including the repository itself:
aws ecr delete-repository --repository-name devstuffs-hello --region us-east-1 --force

Warning

If you’re continuing straight on to the next chapter, don’t delete the repository — Fargate needs a real ECR image to pull from. The next chapter builds directly on this one.

Last updated on