How to Build and Push Docker Images to Amazon ECR with AWS CodeBuild

AWS CodeBuild can build a Docker image and push it to Amazon ECR in a single automated step. This guide gives you a complete, copy-paste-ready buildspec.yml along with the IAM permissions, tagging strategy, and caching setup that make it work reliably in production.

By the end, you will have a working buildspec that logs into ECR, builds your image tagged with the git commit SHA, and pushes it so a downstream CodePipeline deployment stage can consume it.

What is actually happening inside CodeBuild

When CodeBuild builds a Docker image, it runs Docker inside a container. Your build environment is itself a container, and the Docker daemon runs inside that container to execute your docker build command. This pattern is called Docker-in-Docker.

The nesting doll analogy: Think of CodeBuild’s build environment like a contractor working inside a room inside a building. The EC2 host is the building, the CodeBuild container is the room, and the Docker daemon is the contractor setting up their own workspace inside. Privileged mode is the key that lets the contractor bring in their own heavy tools.

Authentication to Amazon ECR uses short-lived tokens. CodeBuild calls aws ecr get-login-password, which returns a credential that expires after 12 hours, and pipes it directly to docker login. No passwords are stored. From that point, docker push works like pushing to any private registry.

If you are new to AWS CodeBuild, read the AWS CodeBuild overview first. It covers buildspec phases and environment variables before you write any Docker-specific configuration.

How the build flow works step by step

  1. Source enters CodeBuild. CodeBuild pulls your source code from GitHub, CodeCommit, S3, or another provider. The full git commit SHA is available as CODEBUILD_RESOLVED_SOURCE_VERSION.
  2. Privileged container starts. The build environment launches with elevated privileges so the Docker daemon can run inside it.
  3. CodeBuild authenticates to ECR. In the pre_build phase, a short-lived ECR token is piped directly into docker login. Never store ECR credentials in environment variables.
  4. Docker image is built. docker build reads your Dockerfile and builds the image layer by layer. Unchanged layers are reused from cache.
  5. Image is tagged. The image gets a commit SHA tag (immutable, unique) and optionally a latest tag as a mutable reference. The commit SHA tag is what you actually deploy.
  6. Image is pushed to ECR. docker push uploads new layers to your ECR repository. Layers already in ECR are skipped.
  7. Artifact is written for downstream stages. An imagedefinitions.json file is created so downstream CodePipeline stages know which image URI to deploy. See deploying with CodeBuild for how that stage consumes this file.

When CodeBuild is the right tool for this

CodeBuild fits well when:

  • You are building a fully AWS-native pipeline with CodePipeline and CodeDeploy.
  • Your build needs to reach resources inside a VPC, such as a private package registry or an internal database during integration tests.
  • Your organization requires all workloads to remain within AWS for compliance or data residency.
  • You want AWS to manage the build runner fleet with no self-hosted infrastructure.

GitHub Actions may be simpler when:

  • Your source is already in GitHub and your team uses GitHub Actions for other workflows.
  • You want a one-file setup without separate IAM roles and CodeBuild project configuration.
  • You need marketplace integrations that have no native CodeBuild equivalent.

See the CodeBuild vs GitHub Actions comparison later on this page for a detailed breakdown.

What you need before you start

Three things need to be in place before you write the buildspec:

  • An ECR repository to push images to.
  • A CodeBuild project with privileged mode enabled.
  • An IAM service role attached to the CodeBuild project with the correct ECR permissions.
# Create an ECR repository
aws ecr create-repository \
  --repository-name my-app \
  --image-scanning-configuration scanOnPush=true \
  --image-tag-mutability IMMUTABLE \
  --region us-east-1

The —image-tag-mutability IMMUTABLE flag prevents any future push from overwriting an existing tag. This protects your git-commit-tagged images from accidental overwrites. For more on ECR repository setup, see the Amazon ECR overview.

Enable privileged mode before anything else

Docker builds inside CodeBuild use Docker-in-Docker. The Docker daemon runs inside your build container, which requires privileged mode to access the kernel capabilities it needs.

In your CodeBuild project, go to Edit → Environment and check “Enable this flag if you want to build Docker images or want your builds to get elevated privileges.” This must be set at the project level. It cannot be enabled from within the buildspec.

If you skip this step, your build will fail with a message like Cannot connect to the Docker daemon at unix:///var/run/docker.sock or permission denied while trying to connect to the Docker daemon socket. Both mean the same thing: privileged mode is off.

# Terraform: CodeBuild project with privileged mode
resource "aws_codebuild_project" "docker_build" {
  name         = "my-app-docker-build"
  service_role = aws_iam_role.codebuild.arn

  artifacts {
    type = "NO_ARTIFACTS"
  }

  environment {
    compute_type    = "BUILD_GENERAL1_SMALL"
    image           = "aws/codebuild/standard:7.0"
    type            = "LINUX_CONTAINER"
    privileged_mode = true

    environment_variable {
      name  = "AWS_ACCOUNT_ID"
      value = data.aws_caller_identity.current.account_id
    }

    environment_variable {
      name  = "ECR_REPO_NAME"
      value = "my-app"
    }
  }

  source {
    type            = "GITHUB"
    location        = "https://github.com/my-org/my-app"
    git_clone_depth = 1
  }
}

Complete buildspec.yml for Docker builds

This buildspec handles everything: ECR login, layer cache pull, image build with commit SHA tag, push, and the artifact file that lets CodePipeline deploy the right image.

version: 0.2

env:
  variables:
    AWS_DEFAULT_REGION: "us-east-1"
    IMAGE_REPO_NAME: "my-app"

phases:
  pre_build:
    commands:
      - AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
      - ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
      - IMAGE_URI="${ECR_REGISTRY}/${IMAGE_REPO_NAME}"
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-8)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
      - echo "Logging in to Amazon ECR..."
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ECR_REGISTRY

  build:
    commands:
      - echo "Building Docker image..."
      - docker pull $IMAGE_URI:latest || true
      - |
        docker build \
          --cache-from $IMAGE_URI:latest \
          --tag $IMAGE_URI:$IMAGE_TAG \
          --tag $IMAGE_URI:latest \
          --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
          --build-arg GIT_COMMIT=$COMMIT_HASH \
          .

  post_build:
    commands:
      - echo "Pushing Docker image..."
      - docker push $IMAGE_URI:$IMAGE_TAG
      - docker push $IMAGE_URI:latest
      - printf '[{"name":"my-app","imageUri":"%s"}]' $IMAGE_URI:$IMAGE_TAG > imagedefinitions.json
      - echo "Build complete. Image $IMAGE_URI:$IMAGE_TAG"

artifacts:
  files:
    - imagedefinitions.json

CODEBUILD_RESOLVED_SOURCE_VERSION is set automatically and contains the full git commit SHA. The cut -c 1-8 trims it to 8 characters. The IMAGE_TAG=${COMMIT_HASH:=latest} fallback handles manual builds triggered without source code attached.

The docker pull … || true line pulls the previous image for layer caching. The || true prevents the build from failing on the very first run, when there is no previous image to pull yet.

If you are also running automated tests in this pipeline, consider separating the test and Docker build steps into distinct CodeBuild projects. See automated testing in CodeBuild for how to structure this cleanly.

Required IAM permissions

The CodeBuild service role needs two groups of ECR permissions: authentication (must be on all resources) and push/pull operations (scoped to the specific repository).

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ECRAuth",
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ECRPush",
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:PutImage",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload"
      ],
      "Resource": "arn:aws:ecr:us-east-1:123456789012:repository/my-app"
    },
    {
      "Sid": "CloudWatchLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}

GetAuthorizationToken must use Resource: ”*”. It is an ECR service-level operation, not a repository operation, so it cannot be scoped to a specific repository ARN. All push and pull permissions can and should be scoped to the repository.

Common copy-paste trap: The repository ARN format is arn:aws:ecr:REGION:ACCOUNT:repository/NAME. The service field is ecr, not iam. Using arn:aws:iam will silently fail to match the resource, and every push will be denied with an access error that looks like a permissions problem but is actually a bad ARN.

If a build still fails with access denied after applying these permissions, check the IAM access denied troubleshooting guide. Common causes include a deny statement in an ECR resource-based policy or an SCP at the AWS Organizations level. For secrets your build pipeline needs beyond ECR credentials, see secrets in CI/CD pipelines.

Docker image tagging strategy

Your image tagging strategy determines whether you can roll back a broken deployment in two minutes or two hours.

The prescription bottle analogy: Using only latest is like relabeling a single prescription bottle every time the formula changes. If a patient has a reaction, there is no way to retrieve the previous version. A commit SHA tag is like stamping every batch with its own lot number — each one is traceable, immutable, and retrievable.

Git commit SHA (recommended)

Tag each image with the git commit SHA that produced it. Every pushed commit creates a unique, immutable image tag. When you roll back, you deploy the image from the last known-good commit SHA. No rebuilding, no guessing.

# Example tags for commit abc12345
my-app:abc12345    # Immutable — always points to this exact build
my-app:main        # Mutable — moves to the latest build on main

Semantic version

For software with formal releases, tag images with the release version such as my-app:1.4.2. Combined with a matching git tag, this creates a clear audit trail from release notes to running image.

latest alone (avoid in production)

Using only latest means every build overwrites the previous image. If your deployment goes down and you need to roll back in under five minutes, rebuilding from source is not a realistic option. Always push an immutable tag alongside latest.

For tagging conventions across dev, staging, and production environments, see dev vs staging vs production environments.

Speeding up builds with layer caching

Each CodeBuild build starts with a fresh container and an empty Docker layer cache. Without caching, every build downloads and rebuilds every layer from scratch. Two strategies help significantly.

Registry-based caching (reliable): Pull the previous image before building and pass it as —cache-from. Docker reuses unchanged layers from the pulled image. The pull takes a few seconds but saves much more time when most layers are unchanged, which is the common case when only application code changes between commits.

Local Docker layer cache (faster, less predictable): Enable LOCAL_DOCKER_LAYER_CACHE in the CodeBuild project cache settings. When CodeBuild reuses the same underlying host between successive builds, the Docker layer cache from the previous build is available with no pull required. The catch is that CodeBuild does not guarantee host reuse, so this cache can be cold.

# Terraform: enable local Docker layer caching
resource "aws_codebuild_project" "docker_build" {
  cache {
    type  = "LOCAL"
    modes = ["LOCAL_DOCKER_LAYER_CACHE", "LOCAL_SOURCE_CACHE"]
  }
}

Use both: configure local caching in project settings and keep the —cache-from pull in your buildspec. When local cache is warm, the pull completes fast. When it is cold, the pull fills the gap.

Multi-architecture builds for Graviton

If you deploy to AWS Graviton (ARM64) instances or ECS Fargate with ARM64 task definitions, you need ARM64 Docker images. A standard linux/amd64 image will not run on ARM64 hardware. Use Docker buildx to build a multi-architecture manifest that serves the correct image to each platform automatically.

phases:
  pre_build:
    commands:
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ECR_REGISTRY
      - docker buildx create --use --name multiarch-builder
      - docker buildx inspect --bootstrap

  build:
    commands:
      - |
        docker buildx build \
          --platform linux/amd64,linux/arm64 \
          --tag $IMAGE_URI:$IMAGE_TAG \
          --push \
          .

The —push flag pushes directly during the build rather than as a separate step. Multi-arch images are stored as manifest lists in ECR, and a standard docker push after the build cannot correctly push a multi-arch manifest. The standard CodeBuild images include QEMU for cross-platform emulation. You do not need to install it separately.

For more on running container workloads across architectures in AWS, see container images in AWS.

Common beginner mistakes

  1. Forgetting privileged mode. Docker builds fail with a daemon socket error if privileged mode is not enabled at the project level. It cannot be set from the buildspec.

  2. Pushing only the latest tag. If your pipeline pushes only latest, rolling back means rebuilding from source. Always push both the commit SHA tag and a mutable tag. Deploy from the SHA tag, not from latest.

  3. Wrong ECR ARN in IAM policies. ECR repository ARNs use arn:aws:ecr:REGION:ACCOUNT:repository/NAME. Using arn:aws:iam is a silent mismatch: the permission appears to exist but never matches the actual resource, and every push is denied.

  4. Missing GetAuthorizationToken on *. Scoping all ECR permissions to a repository ARN and leaving out ecr:GetAuthorizationToken on * causes the login step to fail immediately. See the IAM access denied guide for how to diagnose this quickly.

  5. Not pulling before building. Without docker pull $IMAGE:latest || true followed by —cache-from $IMAGE:latest, every build rebuilds all layers from scratch. The || true prevents the first-run failure when no image exists yet.

AWS CodeBuild vs GitHub Actions for Docker builds

Both tools build and push Docker images to ECR. The right choice depends on where your source lives and how much AWS infrastructure you are already using.

Choose CodeBuild when

  • You are building an AWS-native pipeline with CodePipeline and CodeDeploy.
  • Your build needs to reach resources inside a VPC, such as a private package registry or an internal database during tests.
  • Your organization restricts all workloads to AWS infrastructure for compliance reasons.
  • You want AWS to manage the build runner fleet with no self-hosted machines.

Choose GitHub Actions when

  • Your source is in GitHub and your team already uses GitHub Actions for other workflows.
  • You want a single workflow YAML file without separate IAM roles and CodeBuild project configuration.
  • You need marketplace actions for notifications, security scanning, or integrations that CodeBuild lacks.
  • You prefer OIDC federation for AWS authentication, with no long-lived credentials to rotate.

For teams starting fresh on AWS with source in GitHub, GitHub Actions is often the faster path to a working Docker pipeline. For teams deep in AWS who need VPC-isolated builds or full CodePipeline orchestration, CodeBuild is the better fit. See GitHub Actions for AWS for the OIDC-based ECR push setup, and CI/CD pipelines for ECS for a full end-to-end deployment pipeline comparison. For pipeline security on either path, see secure CI/CD pipelines.

Frequently asked questions

Why does my Docker build fail with permission denied in CodeBuild?

Docker builds inside CodeBuild require privileged mode. Go to your CodeBuild project settings, edit the environment, and enable the privileged mode checkbox. Without it, the Docker daemon cannot start inside the build container. This setting must be enabled at the project level. It cannot be set from within the buildspec.

What IAM permissions does the CodeBuild service role need to push to ECR?

At minimum: ecr:GetAuthorizationToken scoped to * (a service-level operation that cannot be scoped to a repository), plus ecr:BatchCheckLayerAvailability, ecr:GetDownloadUrlForLayer, ecr:BatchGetImage, ecr:PutImage, ecr:InitiateLayerUpload, ecr:UploadLayerPart, and ecr:CompleteLayerUpload scoped to the repository ARN. The correct format is arn:aws:ecr:REGION:ACCOUNT_ID:repository/REPO_NAME. Note: the service field is ecr, not iam.

Should I use the latest tag for production Docker deployments?

No. The latest tag is mutable and always points to the most recently pushed image. If you need to roll back, you cannot reliably pull the previous image because latest has already moved. Always tag images with the git commit SHA so every build produces a unique, immutable tag. You can push latest as a secondary reference tag, but never deploy from it.

How do I speed up Docker builds in CodeBuild?

Two approaches work well together. Registry-based caching: pull the latest image before building and pass it as --cache-from. Docker reuses unchanged layers from the pulled image. Local Docker layer cache: enable LOCAL_DOCKER_LAYER_CACHE in the CodeBuild project cache settings. Local caching is faster but less reliable since CodeBuild does not guarantee host reuse between builds.

When do I need multi-architecture Docker images?

When your deployment targets include AWS Graviton (ARM64) instances or ECS Fargate with ARM64 task definitions. A standard amd64 image will not run on ARM64 hardware. Use docker buildx build with --platform linux/amd64,linux/arm64 to push a multi-arch manifest so ECR serves the correct image automatically at deploy time.

Last verified: 25 April 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.