AWS – CodeBuild: buildspec, Caching and Build Speed

August 19, 20254 min readUpdated 8/24/2026

CodeBuild runs your build in a managed container: it clones the source, runs the commands you declare, and uploads whatever you tell it to keep. You pay per build-minute and nothing when idle.

buildspec.yml, phase by phase

version: 0.2

env:
  variables:
    NODE_ENV: production
  parameter-store:
    NPM_TOKEN: /stayhub/prod/npm-token

phases:
  install:
    runtime-versions:
      nodejs: 20
    commands:
      - npm ci
  pre_build:
    commands:
      - npm run lint
      - npm test
  build:
    commands:
      - npm run build
  post_build:
    commands:
      - echo "built $CODEBUILD_RESOLVED_SOURCE_VERSION"

artifacts:
  files:
    - '**/*'
  base-directory: dist

cache:
  paths:
    - 'node_modules/**/*'

The phases are conventions rather than rules — nothing stops you running tests in build — but keeping to them makes the console's phase timings useful, which is how you find out that install is two thirds of your build.

One real difference: post_build runs even when build fails. That is deliberate, so you can publish test reports or clean up, and it is a trap if you put "push the image" there — it will push after a failed build unless you check $CODEBUILD_BUILD_SUCCEEDING.

The artifacts block decides what the next stage gets

CodeBuild uploads only what artifacts declares. Omit the block and the next pipeline stage receives an empty zip — and it fails there, not here, which is why this shows up as a mysterious deploy failure.

base-directory is the other half. Without it, files: '**/*' uploads your whole workspace including node_modules, producing an artifact hundreds of megabytes larger than it needs to be and a slow upload on every build.

Caching, which is usually the whole performance story

By default every build starts from a clean container and downloads every dependency again. On a typical Node or Python project that is most of the build time and most of the bill.

Two kinds of cache, and they are not alternatives:

  • Local caching — kept on the build host. Fast, free, and only hits when a build lands on a host that ran a recent build. Modes include DOCKER_LAYER_CACHE, SOURCE_CACHE and CUSTOM_CACHE
  • S3 caching — uploaded and downloaded per build. Reliable, hits every time, and costs the transfer. Better for large dependency trees
aws codebuild update-project --name stayhub-build \
  --cache '{"type":"LOCAL","modes":["LOCAL_DOCKER_LAYER_CACHE","LOCAL_SOURCE_CACHE"]}'

Docker layer caching is the biggest single win if you build images, and it requires privileged mode — see below. Measure before and after; a cache that never hits is pure overhead.

Compute type: do not guess

Sizes run from BUILD_GENERAL1_SMALL upwards, and a larger one costs more per minute while finishing sooner. Because you are billed per minute, the bigger instance is sometimes cheaper overall, and sometimes not — it depends entirely on whether your build parallelises.

A single-threaded build gains almost nothing from more vCPUs and you pay the higher rate for the same wall-clock time. A test suite that runs in parallel can halve both. Try two sizes on the same commit and compare; it is a ten-minute experiment that settles the question.

Graviton (ARM_CONTAINER) is cheaper per minute again, if your dependencies build for arm64.

Secrets belong in parameter-store, not variables

Anything under env.variables is plain text in the buildspec, which is in your repository, and it is echoed in the build log. The parameter-store and secrets-manager blocks fetch at build time and mask the value in the log.

The project's service role needs ssm:GetParameters and kms:Decrypt for that to work — and the failure is an unhelpful "parameter not found", which sends people looking for a typo rather than a permission.

Building a container image

Running docker build inside CodeBuild needs privileged mode, which is off by default. Without it, the Docker daemon cannot start and the error mentions a socket rather than a setting.

aws codebuild update-project --name stayhub-build \
  --environment '{"type":"LINUX_CONTAINER","image":"aws/codebuild/standard:7.0","computeType":"BUILD_GENERAL1_SMALL","privilegedMode":true}'

aws ecr get-login-password --region us-west-2 \
  | docker login --username AWS --password-stdin 111122223333.dkr.ecr.us-west-2.amazonaws.com

privilegedMode is nested inside --environment rather than being a flag of its own, and --environment replaces the whole block — so you must restate type, image and computeType or you will reset them to something you did not choose. This is a general property of the update-project call and it catches people on --artifacts and --source too.

Note the ECR login is piped rather than passed as an argument, so the token never appears in the process list or the build log.

What a build costs, and the free tier

Billing is per build-minute at a rate set by compute type, with 100 minutes a month free on the smallest Linux size. For a small project that is often the entire CI bill.

The number worth watching is not the rate but the duration, and the duration is usually dependencies. A build that spends four minutes on npm ci and forty seconds compiling is a caching problem, not a compute-size problem — and buying a larger instance to fix it makes the bill worse rather than better.

Debugging a failing build

aws codebuild batch-get-builds --ids stayhub-build:abc-123 \
  --query "builds[].[buildStatus,phases[?phaseStatus=='FAILED'].[phaseType,contexts[].message]]"

That names the phase that failed, which narrows things immediately: an install failure is dependencies, pre_build is your tests, and a DOWNLOAD_SOURCE failure is the connection or the service role rather than anything in your code.

For anything you cannot reproduce from logs, CodeBuild can pause a build and let you connect to the container with Session Manager — far faster than adding echo statements and pushing again.