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 โ†’ add services: [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/maven caches or define a custom one under definitions.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; main deploys).
  • 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.yml fails 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 script into 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-script to 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.