AWS CodeBuild

By Vishvesh Patel · DevOps EngineerUpdated September 15, 2026

Running builds on AWS CodeBuild: the buildspec file, caching, building Docker images, VPC access, exported variables, and diagnosing a failing build.

CodeBuild runs commands in a managed container and charges by the minute. There are no build servers to patch and no capacity to plan. You define the environment and the commands, and it runs them.

The buildspec

A buildspec.yml in your repository root defines everything:

version: 0.2

env:
  variables:
    NODE_ENV: production
  parameter-store:
    NPM_TOKEN: /build/npm-token
  secrets-manager:
    DB_PASSWORD: prod/payments/db:password
  exported-variables:
    - IMAGE_TAG

phases:
  install:
    runtime-versions:
      nodejs: 22
    commands:
      - npm ci

  pre_build:
    commands:
      - export IMAGE_TAG=${CODEBUILD_RESOLVED_SOURCE_VERSION:0:7}
      - aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_REGISTRY

  build:
    commands:
      - npm run lint
      - npm test -- --ci
      - docker build -t $ECR_REGISTRY/payments-api:$IMAGE_TAG .

  post_build:
    commands:
      - docker push $ECR_REGISTRY/payments-api:$IMAGE_TAG
      - printf '[{"name":"api","imageUri":"%s"}]' "$ECR_REGISTRY/payments-api:$IMAGE_TAG" > imagedefinitions.json

reports:
  unit-tests:
    files: ['junit.xml']
    file-format: JUNITXML

artifacts:
  files:
    - imagedefinitions.json

cache:
  paths:
    - '/root/.npm/**/*'

Several things in there are worth calling out.

parameter-store and secrets-manager fetch values at build time, so nothing sensitive sits in the buildspec or in project environment variables. The CodeBuild service role needs permission to read them, and kms:Decrypt if they are encrypted with a customer-managed key.

exported-variables makes a value available to later CodePipeline stages. This is how a generated image tag reaches the deploy stage without writing it to a file.

reports parses JUnit XML into a test report visible in the console, showing which tests failed rather than requiring you to read the log.

post_build runs even when build fails. Guard anything that should only happen on success with $CODEBUILD_BUILD_SUCCEEDING, or you will push an image from a failed build.

Phases and failure behaviour

PhasePurposeRuns if an earlier phase failed
installRuntime and tooling()
pre_buildLogin, setupNo
buildThe actual workNo
post_buildPublish, notify, clean upYes

That last row is the one that catches people. A failing test does not prevent post_build from executing:

  post_build:
    commands:
      - |
        if [ "$CODEBUILD_BUILD_SUCCEEDING" = "1" ]; then
          docker push $ECR_REGISTRY/payments-api:$IMAGE_TAG
        else
          echo "Build failed, not pushing"
        fi

Building Docker images

A CodeBuild container has no Docker daemon by default. docker build fails with "Cannot connect to the Docker daemon" until you enable privileged mode:

aws codebuild update-project \
  --name payments-build \
  --environment 'type=LINUX_CONTAINER,image=aws/codebuild/standard:7.0,computeType=BUILD_GENERAL1_MEDIUM,privilegedMode=true'

Privileged mode is required and is a genuine trade-off. A privileged container can reach the host kernel. CodeBuild environments are ephemeral and single-tenant, which makes this far less dangerous than privileged mode on a shared runner, but it is still worth being deliberate about which projects have it.

Docker Hub rate limits will bite a busy pipeline. Either authenticate to Docker Hub with credentials from Secrets Manager, or mirror base images into ECR and pull from there.

Caching

Builds start from a clean container every time, so dependency installation is repeated on every run unless you cache.

Local caching is fastest but only helps when the same build host is reused:

--cache 'type=LOCAL,modes=[LOCAL_DOCKER_LAYER_CACHE,LOCAL_SOURCE_CACHE,LOCAL_CUSTOM_CACHE]'

S3 caching persists across hosts, at the cost of upload and download time:

--cache 'type=S3,location=acme-codebuild-cache/payments'

Cache the package manager's download directory rather than the installed tree, /root/.npm rather than node_modules. Restoring a download cache and running npm ci is more reliable than restoring a partially-populated dependency directory across different base images.

Measure it. A large cache over a slow link can cost more than it saves.

VPC access

By default CodeBuild runs outside your VPC, with internet access and no route to private resources. To reach an RDS instance or an internal service, configure VPC access:

aws codebuild update-project --name payments-build \
  --vpc-config 'vpcId=vpc-0abc123,subnets=[subnet-private-a,subnet-private-b],securityGroupIds=[sg-build]'

A build in a VPC has no internet access by default, so it needs a NAT gateway or VPC endpoints. Otherwise it cannot pull dependencies, reach ECR, or write logs, and it fails in ways that look unrelated to the network change. This is the same trap as VPC-attached Lambda.

Use private subnets; a public subnet does not give a CodeBuild ENI internet access.

Compute and cost

Billing is per build minute at a rate that scales with compute type. Counterintuitively, a larger instance is often cheaper overall, because a build that takes half as long on twice the compute costs about the same and returns feedback sooner.

ARM compute types are cheaper still where your build supports them.

Build timeout defaults to 60 minutes and caps at 8 hours. Set it to something realistic. A hung build otherwise burns an hour of billing before failing.

Diagnosing failures

Build logs go to CloudWatch:

aws logs tail /aws/codebuild/payments-build --follow
SymptomCause
Cannot connect to the Docker daemonprivilegedMode not enabled
AccessDenied on AWS callsThe CodeBuild service role lacks the permission (not the pipeline role)
Cannot reach the internetThe project is in a VPC with no NAT or endpoints
toomanyrequests pulling an imageDocker Hub rate limit (authenticate or mirror to ECR)
Cache never restoredCache paths outside the build directory, or a changing cache key
Build succeeds but nothing deployedartifacts.files does not include the expected file

For anything you cannot reproduce from logs, break the session into an interactive shell:

phases:
  build:
    commands:
      - codebuild-breakpoint

With session debugging enabled on the project, the build pauses and you can connect via SSM to inspect the environment. It is considerably faster than adding echo statements and re-running.

Quick reference

TaskCommand
Start a buildaws codebuild start-build --project-name <n>
Build statusaws codebuild batch-get-builds --ids <build-id>
Recent buildsaws codebuild list-builds-for-project --project-name <n>
Tail logsaws logs tail /aws/codebuild/<project> --follow
Enable privileged modeaws codebuild update-project --name <n> --environment '...privilegedMode=true'
Override a variableaws codebuild start-build --project-name <n> --environment-variables-override name=TAG,value=v1
Stop a buildaws codebuild stop-build --id <build-id>

Key takeaways

  • post_build runs even when the build failed. Guard it with $CODEBUILD_BUILD_SUCCEEDING.
  • Docker builds need privilegedMode, since there is no daemon by default.
  • Pull secrets with parameter-store and secrets-manager, not project environment variables.
  • exported-variables passes values to later CodePipeline stages.
  • A VPC-attached build loses internet access: add NAT or VPC endpoints.
  • Cache the package manager's download directory, not the installed dependency tree.
  • A larger compute type is often cheaper because the build finishes sooner.
  • Permission errors are the CodeBuild service role, not the pipeline role.

Check your understanding

Exercise

An image pushed from a failed build

Tests fail in the build phase, yet the Docker image is still pushed to ECR by post_build. Why?

Exercise

A build that broke after a VPC change

A working build starts failing to install npm packages immediately after VPC access was configured on the project. Why?


Frequently Asked Questions

What is a buildspec file and where does it go?

buildspec.yml in the root of your repository defines the build: environment variables, runtime versions, and the commands to run in each phase. Keeping it in the repository means the build definition is versioned and reviewed alongside the code it builds. You can also define it inline in the project or point at a different path, but the repository file is the default and the better practice.

Why does docker build fail in CodeBuild?

Because a CodeBuild container has no Docker daemon unless you enable privilegedMode on the project's environment. Without it, docker build reports "Cannot connect to the Docker daemon". Privileged mode is required for Docker-in-Docker builds and is a real trade-off, though less risky here than on shared runners since CodeBuild environments are ephemeral and single-tenant. Enable it only on projects that genuinely build images.

Why does my build run even though tests failed?

Because post_build executes regardless of whether earlier phases succeeded, that is deliberate, so it can publish logs and notifications after a failure. Anything that should only happen on success needs to check the $CODEBUILD_BUILD_SUCCEEDING environment variable, which is 1 on success and 0 on failure. Forgetting this guard is how an image from a failed build ends up pushed to a registry.

How do I pass secrets into a CodeBuild build?

Use the parameter-store or secrets-manager blocks in the buildspec's env section, which fetch values at build time and expose them as environment variables. Nothing sensitive then appears in the buildspec, the project configuration or your repository. The CodeBuild service role needs permission to read them, plus kms:Decrypt if they are encrypted with a customer-managed key, and that is the role to check when access is denied, not the CodePipeline role.

Why did my build lose internet access after enabling VPC configuration?

Because outside a VPC, CodeBuild runs on AWS-managed networking with internet access included; attaching it to your VPC puts it behind your routing rules. A private subnet without a NAT gateway has no route to public package registries, and without VPC endpoints it cannot reach ECR, Secrets Manager or CloudWatch Logs either. Add a NAT gateway, or add interface endpoints for the AWS services and NAT for anything public. Use private subnets. A public subnet does not give the build ENI internet access.

How do I make CodeBuild faster?

Cache dependencies, using the package manager's download directory rather than the installed tree. Consider a larger compute type, which is frequently cheaper overall because the build finishes in less than half the time at roughly twice the rate. Use ARM compute where your build supports it. Mirror frequently-pulled base images into ECR to avoid Docker Hub rate limits and pull latency. And set a realistic build timeout, since the 60-minute default lets a hung build bill for an hour before failing.

Continue Learning

Explore Related Topics

When it breaks

Related Resources