Bitbucket Pipelines is Atlassian's built-in CI/CD: one bitbucket-pipelines.yml at your repo root and
every push builds in a container. If you know Docker, you already know most of it. This guide takes you
from first build to a gated production deploy.
Prerequisites
- A Bitbucket repository with Pipelines enabled (Repository settings โ Pipelines โ Enable).
- An app with a test command.
- For deploys: a cloud account where you can create a role (for keyless OIDC).
The model
Everything runs inside a Docker image you choose. A pipeline is a list of steps; each step runs in a clean container and passes files to the next via artifacts.
image: node:20
pipelines:
default:
- step:
name: Test
caches: [node]
script:
- npm ci
- npm test
default runs on every push to any branch. The node cache persists node_modules between runs.
Branch-specific pipelines and built-in variables
pipelines:
branches:
main:
- step:
name: Test
caches: [node]
script: [npm ci, npm test]
- step:
name: Build image
services: [docker]
script:
- docker build -t myapp:$BITBUCKET_COMMIT .
$BITBUCKET_COMMIT, $BITBUCKET_BRANCH, and friends are injected automatically โ handy for tagging.
Services: real databases for tests
Need Postgres for integration tests? Declare it as a service and it runs as a sidecar container:
- step:
name: Integration
services: [postgres]
script: [npm run test:int]
definitions:
services:
postgres:
image: postgres:16
variables:
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
Artifacts: pass build output between steps
- step:
name: Build
script: [npm ci, npm run build]
artifacts: [dist/**] # available to later steps
- step:
name: Deploy
script: [./deploy.sh dist]
Deployments and manual gates
Wrap a deploy step with deployment: for tracking, and make production a button with
trigger: manual:
- step:
name: Deploy prod
deployment: production
trigger: manual # never ships automatically
script: [./deploy.sh]
Add reviewers/restrictions under Repository settings โ Deployments.
Secrets and keyless cloud auth
Add secrets as repository variables and mark them Secured (hidden in logs). Better still, use OIDC: Bitbucket signs a token your cloud trusts, so you assume a role with no stored keys:
- step:
name: Deploy
oidc: true
script:
- export AWS_ROLE_ARN=arn:aws:iam::123456789012:role/bb-deployer
- export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/web-identity-token
- echo $BITBUCKET_STEP_OIDC_TOKEN > $AWS_WEB_IDENTITY_TOKEN_FILE
- aws s3 sync dist s3://my-bucket
A complete pipeline
image: node:20
pipelines:
branches:
main:
- step:
name: Test
caches: [node]
services: [postgres]
script: [npm ci, npm test]
- step:
name: Build
caches: [node]
script: [npm run build]
artifacts: [dist/**]
- step:
name: Deploy production
deployment: production
trigger: manual
oidc: true
script: [./deploy.sh dist]
definitions:
services:
postgres: {image: postgres:16, variables: {POSTGRES_PASSWORD: secret}}
Verify it worked
- The Pipelines page shows the run with Test โ Build โ Deploy, the last one waiting for a click.
- Trigger the manual deploy; the Deployments page records it against
production. - Push a failing test and confirm the pipeline stops before Build.
Troubleshooting
docker: command not foundโ addservices: [docker]to any step that runs Docker.- Cache doesn't speed things up โ the cache path changed, or you're caching the wrong directory.
Use the built-in
node/pip/mavencaches or define a custom one underdefinitions.caches. - Out of build minutes โ parallel steps finish faster but cost the same minutes; trim redundant steps and cache aggressively. Watch usage under the workspace's plan.
- Secret printed in logs โ it wasn't marked Secured. Re-add it as a secured variable and rotate.
- Step can't see build output โ you didn't declare
artifacts:on the producing step. - OIDC role denied โ the role's trust condition doesn't match your workspace/repo UUID; copy it exactly from Bitbucket's OIDC settings.
Parallel steps, custom pipelines & runners
A few features turn a basic pipeline into a fast, flexible one.
Parallel steps run independent work at the same time to cut wall-clock time. Group them under
parallel โ lint, unit tests, and a security scan can all run together instead of in sequence:
- parallel:
- step: {name: Lint, script: [npm run lint]}
- step: {name: Unit, script: [npm test]}
- step: {name: Audit, script: [npm audit]}
They share your build-minute budget, but you get the result sooner โ worth it for feedback speed.
Custom pipelines are named pipelines you trigger by hand or on a schedule, rather than on push โ perfect for a nightly integration run or a one-click "deploy this branch to staging":
pipelines:
custom:
nightly-e2e:
- step: {script: [npm run test:e2e]}
branches:
main:
- step: {script: [npm ci, npm test]}
Schedule nightly-e2e under Pipelines โ Schedules, or run it from the UI.
Step size and runners. Each step gets a container with a memory limit; declare size: 2x (or 4x)
for memory-hungry builds. Bitbucket's cloud runners are fine for most work, but for private
infrastructure, larger machines, or special hardware you can register self-hosted runners and route
steps to them with runs-on labels. Watch the per-step 120-minute cap โ long jobs should be split.
Change-based conditions skip steps when nothing relevant changed, saving minutes in a monorepo:
- step:
name: Build frontend
condition:
changesets:
includePaths: ["frontend/**"]
script: [npm --prefix frontend run build]
Deployment environments, tracking & rollbacks
Bitbucket's Deployments feature turns deploy steps into a first-class, auditable history. Tag steps
with a deployment: name (test, staging, production) and Bitbucket tracks what commit is live
where, who deployed it, and when โ visible on the Deployments dashboard.
- step:
name: Deploy staging
deployment: staging
script: [./deploy.sh staging]
- step:
name: Deploy production
deployment: production
trigger: manual # a button, gated by reviewers
script: [./deploy.sh production]
Under Repository settings โ Deployments you add deployment permissions (who may deploy to
production) and environment variables scoped per environment, so staging and production secrets never
mix. For rollbacks, because each deployment is tied to a commit and an immutable image tag
($BITBUCKET_COMMIT), rolling back is redeploying the previous known-good tag โ either by re-running an
older successful deployment or triggering the pipeline against the last good commit. This is exactly why
you tag images by commit rather than latest.
Pipes: reusable integrations
Pipes are Bitbucket's version of reusable actions โ pre-packaged integrations you drop into a step instead of scripting them by hand. There are official pipes for pushing to AWS/GCP/Azure, deploying to Kubernetes, sending Slack notifications, scanning for security issues, and more:
- step:
name: Notify
script:
- pipe: atlassian/slack-notify:2.0.0
variables:
WEBHOOK_URL: $SLACK_WEBHOOK
MESSAGE: "Deployed $BITBUCKET_COMMIT to production"
Pipes keep your YAML small and consistent, and you can write your own (a pipe is just a Docker image with a defined interface) to standardize a step across many repositories.
Test reports, coverage & quality gates
A pipeline should do more than pass or fail โ it should surface why. Bitbucket automatically parses
test reports if your step writes JUnit-style XML into test-results/, showing failed tests inline on
the pipeline result. Pair that with a coverage report and, crucially, quality gates: fail the
build when coverage drops below a threshold or a linter finds errors, so standards are enforced by the
machine, not by reviewer memory. Add a security scan pipe to gate on vulnerable dependencies. The
principle is the same as any good CI: make the pipeline the objective arbiter of "is this mergeable?"
Bitbucket Pipelines vs GitHub Actions vs Jenkins
Choosing (or inheriting) a CI tool is common; here's the honest comparison.
| Bitbucket Pipelines | GitHub Actions | Jenkins | |
|---|---|---|---|
| Setup | Zero โ built into Bitbucket | Zero โ built into GitHub | You host and maintain it |
| Config | bitbucket-pipelines.yml |
.github/workflows/*.yml |
Jenkinsfile (Groovy) |
| Runners | Cloud + self-hosted | Cloud + self-hosted | Your own agents |
| Ecosystem | Pipes | Huge Marketplace | Vast plugin ecosystem |
| Best when | You're on Bitbucket/Jira | You're on GitHub | Full control / complex legacy |
If your code is already on Bitbucket (common with Jira/Atlassian shops), Pipelines is the frictionless choice. GitHub Actions has the largest action ecosystem. Jenkins gives ultimate flexibility at the cost of running and securing it yourself. The concepts โ steps, caches, artifacts, gated deploys, secrets โ transfer directly between all three, so learning one makes the others easy.
Caches & artifacts in depth
These two are constantly confused; using them right is most of what makes a pipeline fast and correct.
Caches speed things up; they are not guaranteed. A cache (dependencies, a build cache) is an
optimization โ Bitbucket may or may not have it, and your pipeline must work without it. Use the built-in
caches (node, pip, maven, docker) or define custom ones keyed on a directory:
definitions:
caches:
nextcache: frontend/.next/cache
Artifacts move required files between steps and are guaranteed within a pipeline run. A build step
produces dist/; a later deploy step consumes it. If a step needs a file an earlier step made, it's an
artifact, not a cache:
- step: {name: Build, script: [npm run build], artifacts: [dist/**]}
- step: {name: Deploy, script: [./deploy.sh dist]}
Rule of thumb: cache = "nice to have, rebuildable"; artifact = "must have, produced here." Mixing them up causes the classic "works locally, missing file in CI" failure.
A worked monorepo pipeline
Real repos hold several services, and you don't want to build all of them on every change. Combine branch pipelines, path conditions, and parallel steps:
pipelines:
branches:
main:
- parallel:
- step:
name: API
condition: {changesets: {includePaths: ["api/**"]}}
caches: [node]
script: [npm --prefix api ci, npm --prefix api test]
- step:
name: Web
condition: {changesets: {includePaths: ["web/**"]}}
caches: [node]
script: [npm --prefix web ci, npm --prefix web test]
- step:
name: Deploy changed services
deployment: production
trigger: manual
script: [./deploy-changed.sh]
A docs-only change now skips both build steps; an API change runs only the API step. This keeps a monorepo's CI fast and cheap as it grows.
Triggers: pushes, PRs, tags & schedules
Bitbucket runs different pipelines for different events, which is how you separate "test everything on a PR" from "deploy on a tag":
branches:โ run on pushes to matching branches (feature branches test;maindeploys).pull-requests:โ run on PRs, ideal for the full test suite and quality gates before merge.tags:โ run on git tags, the clean trigger for a release pipeline (build the versioned artifact, deploy production).custom:+ Schedules โ manual or scheduled runs (nightly e2e, a weekly dependency audit).
Matching the right pipeline to the right event keeps fast feedback on PRs while reserving heavy deploys for deliberate, traceable triggers like tags.
Validating and testing your pipeline locally
Nothing is slower than debugging CI by pushing "fix pipeline" commits and waiting for a runner. Tighten the loop:
- Validate the YAML before you push. Bitbucket has an online Pipelines validator, and your editor's
YAML linter catches indentation errors (the #1 cause of a pipeline that won't start). A malformed
bitbucket-pipelines.ymlfails instantly with a parse error. - Run the steps locally in the same image. Because every step is just a Docker image plus a script, you
can reproduce it exactly:
docker run -it --rm -v $(pwd):/app -w /app node:20 sh -c "npm ci && npm test". If it passes there, it'll pass in Pipelines โ the environment is identical. - Test one step at a time. When a specific step fails, copy its
scriptinto that local container and iterate there, not in CI. - Keep secrets out of the equation locally by using dummy values, then confirm the real secured variables are set in the repo before the first real run.
This habit turns "ten pushes to get the pipeline green" into "it worked on the first push" โ faster and far less frustrating.
Tips
- Keep images small and specific โ the image is your build environment.
- Use
after-scriptto publish test reports even when a step fails. - Reuse config with YAML anchors (
&name/*name) for repeated steps.
Where to go next
That's the whole surface: image โ steps โ caches โ services โ artifacts โ gated deployments โ OIDC. Our free end-to-end course builds and deploys a real app with a manual production gate and keyless cloud access, guided step by step in a lab.