Introduction
Bitbucket is Atlassian's Git hosting platform, built around the same workspace and project model as Jira and Confluence. What sets it apart from a plain Git host is Pipelines — CI/CD defined as code in a single bitbucket-pipelines.yml file, executed in ephemeral Docker containers, with no separate CI server to install or maintain.
This guide covers Bitbucket the way a team actually uses it day to day: creating workspaces and repositories, protecting branches with permissions and merge checks, writing Pipelines configurations that build, test, and deploy, gating merges on green builds and required reviewers, and shipping to a real server over SSH when a pull request lands on main.
Who this is for
- Engineers whose team already lives in Jira/Confluence and hosts code on Bitbucket.
- Anyone setting up CI/CD who wants pipelines-as-code without running Jenkins.
- DevOps engineers standardizing branch permissions and deployment gates across repos.
Prerequisites
You should have the following before working through the commands in this guide:
- A Bitbucket Cloud account and membership in (or admin of) a workspace.
- Git installed locally and basic familiarity with git add/commit/push/branch.
- A target server (or cloud VM) reachable over SSH, for the deployment examples.
- Comfort reading YAML — Pipelines configuration is plain YAML with a fixed schema.
Cloud vs. Server/Data Center
This guide focuses on Bitbucket Cloud, which is what most teams run today. Where Bitbucket Server/Data Center (self-hosted) differs meaningfully — mainly around Pipelines availability — it is called out explicitly.Theory
Git hosting plus built-in CI/CD
At its core Bitbucket is a hosted Git server: it stores repositories, enforces access control, and renders pull requests. Pipelines extends that with CI/CD that lives next to the code — a bitbucket-pipelines.yml file at the repo root defines what runs on every push, on pull requests, and on manual triggers. There is no separate CI product to provision, connect, or keep patched; enabling Pipelines on a repository is a toggle in settings.
The workspace / project / repository hierarchy
| Level | Purpose |
|---|---|
| Workspace | Top-level container — billing, members, and shared workspace-level settings live here |
| Project | Groups related repositories inside a workspace (e.g. all services for one product) |
| Repository | A single Git repo, with its own branches, permissions, and Pipelines config |
How it compares to GitHub Actions and GitLab CI
The concepts map closely across all three platforms, which makes moving between them mostly a syntax exercise rather than a mental model change:
| Concept | Bitbucket Pipelines | GitHub Actions | GitLab CI |
|---|---|---|---|
| Config file | bitbucket-pipelines.yml | .github/workflows/*.yml | .gitlab-ci.yml |
| Unit of work | Step | Job / Step | Job |
| Execution environment | Docker image per step | Runner + optional container | Docker image per job |
| Reusable trigger logic | branches / pull-requests keys | on: events | rules: / only/except |
| Reusable pipeline logic | YAML anchors / definitions | Reusable workflows / composite actions | extends / includes |
| Marketplace equivalent | Pipes | Actions Marketplace | CI/CD Catalog |
Build minutes are the resource you budget
Pipelines is metered in build minutes per workspace, not per-repo compute you provision yourself. Plan concurrency and caching with that limit in mind — see Troubleshooting.Architecture
A push (or a pull request event) is matched against the trigger rules in bitbucket-pipelines.yml. A matching pipeline spins up a fresh Docker container per step, runs the configured commands, and — if the branch and step qualify — runs a deployment step that pushes the build output to a target environment:
Push/PR → Pipelines match → containerized build/test → conditional deploy over SSH
Steps are isolated by default
Each step in a pipeline gets its own clean container and its own filesystem. Anything produced in one step (a compiled binary, a build directory) is invisible to the next step unless you explicitly declare it as anartifacts entry.Installation
"Installing" Bitbucket CI/CD means creating a repository (inside a workspace) and flipping Pipelines on — there is no server-side agent to install for Cloud.
# Clone a repo you already created in the Bitbucket UI
git clone git@bitbucket.org:my-workspace/my-repo.git
cd my-repo
# Add a minimal pipelines file and push it
cat > bitbucket-pipelines.yml <<'YAML'
image: node:20
pipelines:
default:
- step:
name: Build and test
caches:
- node
script:
- npm ci
- npm test
YAML
git add bitbucket-pipelines.yml
git commit -m "ci: add pipelines config"
git push origin mainEnabling Pipelines
In the Bitbucket Cloud UI: Repository settings → Pipelines → Settings → Enable Pipelines. Once enabled, any commit that adds a validbitbucket-pipelines.yml starts triggering runs automatically.Configuration
Branch permissions
Under Repository settings → Branch permissions, restrict who can push directly and who can merge, per branch or branch pattern:
- Prevent direct pushes to
main— require changes to land via pull request. - Require a minimum number of approvals before a PR can merge.
- Require that the build status is successful before merging is allowed.
- Restrict merge access on release branches (e.g.
release/*) to a specific group.
Repository variables and secured variables
Under Repository settings → Repository variables, define values referenced in bitbucket-pipelines.yml as $VARIABLE_NAME. Marking a variable Secured encrypts it at rest and masks its value in build logs:
- step:
name: Deploy
script:
- echo "Deploying with key length ${#DEPLOY_SSH_KEY}"
- ssh -i <(echo "$DEPLOY_SSH_KEY") deploy@$STAGING_HOST "deploy.sh"Secured variables are masked, not un-loggable
Bitbucket masks a secured variable's exact value in logs, but avoid piping it through commands that transform or partially print it (base64 decode into an echo, substring extraction) — the transformed output is not masked.SSH keys for deployment
Under Repository settings → Pipelines → SSH keys, Bitbucket generates (or lets you upload) a dedicated key pair for that repository. Add the public key as an authorized key on the deployment target; Pipelines injects the private key automatically at ~/.ssh/id_rsa inside build containers, and also adds the host to known_hosts via the configured fingerprint:
# Paste the Pipelines-generated public key into the deploy user's authorized_keys
sudo -u deploy mkdir -p /home/deploy/.ssh
echo "ssh-rsa AAAA... bitbucket-pipelines" | sudo -u deploy tee -a /home/deploy/.ssh/authorized_keys
sudo -u deploy chmod 700 /home/deploy/.ssh
sudo -u deploy chmod 600 /home/deploy/.ssh/authorized_keysCommands
Git commands in a Bitbucket workflow
# Clone via SSH (recommended over HTTPS for daily work)
git clone git@bitbucket.org:my-workspace/my-repo.git
# Create a feature branch and push it, opening a PR from the CLI push output link
git checkout -b feature/payment-retry
git push -u origin feature/payment-retry
# Keep a feature branch current with main before requesting review
git fetch origin
git rebase origin/main
# Tag a release (often consumed by a pipelines "tags" trigger)
git tag -a v1.4.0 -m "Release 1.4.0"
git push origin v1.4.0
# Add or update the SSH remote if you cloned over HTTPS originally
git remote set-url origin git@bitbucket.org:my-workspace/my-repo.gitPipelines YAML syntax basics
Every bitbucket-pipelines.yml is built from the same handful of keys:
| Key | Purpose |
|---|---|
image | Default Docker image for all steps (overridable per step) |
pipelines.default | Runs on every push that doesn't match a more specific branch rule |
pipelines.branches | Runs matched by branch name/pattern (e.g. main, release/*) |
pipelines.pull-requests | Runs when a PR is opened/updated targeting the matched branch |
pipelines.tags | Runs matched by tag name/pattern (e.g. v*) |
step.script | Ordered shell commands executed inside the step's container |
step.caches | Named directories persisted between pipeline runs (e.g. node, docker) |
step.artifacts | Files/globs carried forward from this step into later steps |
step.deployment | Tags the step against a named environment (Test/Staging/Production) |
definitions | Reusable step/service blocks referenced elsewhere via YAML anchors |
image: node:20
pipelines:
default:
- step:
name: Build and test
script:
- npm ci
- npm test
branches:
main:
- step:
name: Build
script:
- npm ci
- npm run build
artifacts:
- dist/**
pull-requests:
'**':
- step:
name: Lint and test
script:
- npm ci
- npm run lint
- npm testExamples
A complete pipeline for a Node.js service: build and test on every branch, then deploy to a Staging environment when main is updated.
image: node:20
definitions:
caches:
npm-cache: node_modules
steps:
- step: &build-and-test
name: Build and test
caches:
- node
script:
- npm ci
- npm run lint
- npm test
- npm run build
artifacts:
- dist/**
pipelines:
default:
- step: *build-and-test
pull-requests:
'**':
- step: *build-and-test
branches:
main:
- step: *build-and-test
- step:
name: Deploy to Staging
deployment: staging
script:
- pipe: atlassian/ssh-run:0.8.1
variables:
SSH_USER: $STAGING_SSH_USER
SERVER: $STAGING_HOST
COMMAND: 'cd /srv/app && ./scripts/deploy.sh'
- pipe: atlassian/rsync-deploy:0.6.1
variables:
USER: $STAGING_SSH_USER
SERVER: $STAGING_HOST
REMOTE_PATH: '/srv/app/dist'
LOCAL_PATH: 'dist'Push triggers the matching pipeline
A push tomainmatchespipelines.branches.maininstead ofdefault— Bitbucket picks the most specific matching rule.Build and test runs first
The&build-and-testanchor is reused via*build-and-testacrossdefault,pull-requests, andbranches.mainso the exact same steps run everywhere, avoiding drift between what a PR tests and what actually ships.Artifacts carry the build forward
dist/**declared as anartifactsentry in the first step makes the compiled output available to the deployment step's fresh container, which otherwise starts empty.Deployment step is tagged to an environment
deployment: stagingassociates the step with the Staging environment in Deployments, giving it its own audit history and, optionally, its own restricted variables.
Real World Example
Scenario: a pull request must pass build and test before it can merge, and merging into main should automatically deploy to a staging server over SSH — no manual deploy step, no separate release ticket.
image: node:20
pipelines:
pull-requests:
'**':
- step:
name: Build and test PR
caches:
- node
script:
- npm ci
- npm test
branches:
main:
- step:
name: Build
caches:
- node
script:
- npm ci
- npm run build
artifacts:
- dist/**
- step:
name: Deploy to Staging
deployment: staging
script:
- apt-get update && apt-get install -y rsync openssh-client
- rsync -az --delete dist/ $STAGING_SSH_USER@$STAGING_HOST:/srv/app/dist
- ssh $STAGING_SSH_USER@$STAGING_HOST "sudo systemctl restart app"Engineer opens a pull request
Thepull-requestspipeline runs automatically and posts a build status onto the PR; a required merge check blocks the merge button until it passes.Reviewer approves and merges
Branch permissions onmainrequire at least one approval and a green build — both are satisfied before the merge is even allowed.Merge triggers the main pipeline
The merge commit lands onmain, matchingpipelines.branches.main, which rebuilds and produces thedist/artifact.Deployment step ships to staging over SSH
rsynccopies the build output to the staging host using the Pipelines-managed SSH key, then a remotesystemctl restartpicks up the new code.Deployment history is recorded
Because the step is taggeddeployment: staging, it shows up under Deployments → Staging with a timestamp, commit, and status — giving a clear audit trail of what is currently live.
Common Issues
Pipeline stuck in "queued"
This usually means the workspace has exhausted its monthly build minutes, or all available parallel runners are busy. Check Workspace settings → Plan details → Pipelinesfor remaining minutes before assuming it's a Bitbucket outage.SSH deploy step fails with "Permission denied (publickey)"
The deployment target'sauthorized_keys almost never has the Pipelines-generated public key, or it has the wrong file permissions (must be 700 on .ssh, 600 on authorized_keys). Re-copy the exact key shown under Repository settings → Pipelines → SSH keys."Pipeline failed to validate" before any step runs
Bitbucket validatesbitbucket-pipelines.yml against its schema before scheduling anything. The most common causes are inconsistent indentation, a step missing its name or script key, or using an anchor before it is defined earlier in the file.Cache doesn't seem to speed anything up
Caches are best-effort and can be evicted, and a cache only helps the specific directory you declared (e.g.node caches node_modules at a fixed path) — a custom install path needs a custom cache definition, not the built-in node cache.Pipeline never triggers on a feature branch
Branch patterns underpipelines.branches are matched literally or by glob — release/* does not match release/1.0/hotfix (only one path segment). Check the exact glob against the branch name, and remember pull-requests and branches are evaluated independently.Best Practices
- Reuse step definitions with YAML anchors (
&name/*name) so PR checks and production builds run identical steps. - Always declare
artifactsexplicitly — never assume a later step can see a previous step's filesystem. - Use
cachesfor dependency directories (node,docker,pip) to cut build minutes meaningfully. - Name deployment steps with
deployment: <environment>so every release shows up in the Deployments dashboard with a real audit trail. - Keep the pull-request pipeline fast (lint + unit tests) and push slower integration/E2E suites to the branch pipeline or a scheduled pipeline.
- Pin Docker image tags (e.g.
node:20.11) rather than floating tags likelatest, so a base image update can't silently break builds.
Security Hardening
- Secured variables — store every credential (API tokens, deploy passwords, SSH passphrases) as a Secured repository or deployment variable, never in plain YAML or committed files.
- Branch permissions + required approvals — block direct pushes to
main/release branches and require at least one reviewer approval plus a passing build before merge. - Deployment environment restrictions — scope which branches can deploy to Production, and require manual triggering (deployment gate) for production deploys rather than fully automatic.
- SSH key rotation — rotate the Pipelines-managed SSH key periodically under Repository settings → Pipelines → SSH keys, and revoke it immediately from every target server if a repository's access changes.
- Least-privilege deploy users — the SSH user Pipelines deploys as should only have permissions on the application directory and restart command it needs, not broad sudo.
Interview Questions
Cheat Sheet
# --- Minimal pipeline ---
image: node:20
pipelines:
default:
- step:
script:
- npm ci
- npm test
# --- Branch-specific pipeline ---
pipelines:
branches:
main:
- step:
script: [npm ci, npm run build]
'release/*':
- step:
script: [npm ci, npm run build]
# --- Pull request pipeline ---
pipelines:
pull-requests:
'**':
- step:
script: [npm ci, npm test]
# --- Tag-triggered pipeline ---
pipelines:
tags:
'v*':
- step:
script: [npm ci, npm run release]
# --- Caches and artifacts ---
- step:
caches:
- node
script:
- npm ci
- npm run build
artifacts:
- dist/**
# --- Deployment step ---
- step:
name: Deploy to Production
deployment: production
trigger: manual
script:
- pipe: atlassian/ssh-run:0.8.1
variables:
SSH_USER: $PROD_SSH_USER
SERVER: $PROD_HOST
COMMAND: './deploy.sh'
# --- Reusable step via YAML anchor ---
definitions:
steps:
- step: &test
script: [npm ci, npm test]
pipelines:
default:
- step: *test
# --- Common git commands for Bitbucket workflows ---
git clone git@bitbucket.org:workspace/repo.git
git checkout -b feature/x && git push -u origin feature/x
git tag -a v1.0.0 -m "release" && git push origin v1.0.0Summary
Bitbucket pairs familiar Git hosting with Pipelines-as-code, so CI/CD configuration travels with the repository instead of living in a separate system. The pieces that make it production-grade are the same ones covered here: branch permissions and required approvals gating what reaches main, secured variables and rotated SSH keys keeping deployment credentials safe, and a bitbucket-pipelines.yml that reuses build/test steps across pull requests and branches so what gets tested is exactly what gets shipped. Practice this end to end on a throwaway repository — open a PR, watch the build gate the merge, then watch the merge itself trigger a real deploy.
Resources
- Bitbucket Cloud documentation —
support.atlassian.com - Bitbucket Pipelines configuration reference —
bitbucket.org - Atlassian Pipes marketplace (SSH, rsync, cloud deploy pipes) —
bitbucket.org - Atlassian Community forums —
community.atlassian.com - Atlassian status page —
status.atlassian.com