Skip to content

Bitbucket: Repositories, Pipelines & Deployments

Repository governance, branch permissions, and Pipelines-as-code — shipping straight from a pull request to production.

BitbucketBitbucket PipelinesSSH
RR

Ranjith R

Linux SysAdmin & Cloud Engineer

Published March 22, 2026Updated April 10, 2026
8 min readBeginner#Bitbucket#Git#CI/CD#Pipelines
Share
Section 01

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.
Section 02

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.
Section 03

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

LevelPurpose
WorkspaceTop-level container — billing, members, and shared workspace-level settings live here
ProjectGroups related repositories inside a workspace (e.g. all services for one product)
RepositoryA 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:

ConceptBitbucket PipelinesGitHub ActionsGitLab CI
Config filebitbucket-pipelines.yml.github/workflows/*.yml.gitlab-ci.yml
Unit of workStepJob / StepJob
Execution environmentDocker image per stepRunner + optional containerDocker image per job
Reusable trigger logicbranches / pull-requests keyson: eventsrules: / only/except
Reusable pipeline logicYAML anchors / definitionsReusable workflows / composite actionsextends / includes
Marketplace equivalentPipesActions MarketplaceCI/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.
Section 04

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 an artifacts entry.
Section 05

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.

terminal
# 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 main

Enabling Pipelines

In the Bitbucket Cloud UI: Repository settings → Pipelines → Settings → Enable Pipelines. Once enabled, any commit that adds a valid bitbucket-pipelines.yml starts triggering runs automatically.
Section 06

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:

bitbucket-pipelines.yml (excerpt)
- 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:

terminal (on the deployment target)
# 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_keys
Section 07

Commands

Git commands in a Bitbucket workflow

terminal
# 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.git

Pipelines YAML syntax basics

Every bitbucket-pipelines.yml is built from the same handful of keys:

KeyPurpose
imageDefault Docker image for all steps (overridable per step)
pipelines.defaultRuns on every push that doesn't match a more specific branch rule
pipelines.branchesRuns matched by branch name/pattern (e.g. main, release/*)
pipelines.pull-requestsRuns when a PR is opened/updated targeting the matched branch
pipelines.tagsRuns matched by tag name/pattern (e.g. v*)
step.scriptOrdered shell commands executed inside the step's container
step.cachesNamed directories persisted between pipeline runs (e.g. node, docker)
step.artifactsFiles/globs carried forward from this step into later steps
step.deploymentTags the step against a named environment (Test/Staging/Production)
definitionsReusable step/service blocks referenced elsewhere via YAML anchors
bitbucket-pipelines.yml (skeleton)
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 test
Section 08

Examples

A complete pipeline for a Node.js service: build and test on every branch, then deploy to a Staging environment when main is updated.

bitbucket-pipelines.yml
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'
  1. Push triggers the matching pipeline

    A push to main matches pipelines.branches.main instead of default — Bitbucket picks the most specific matching rule.
  2. Build and test runs first

    The &build-and-test anchor is reused via *build-and-test across default, pull-requests, and branches.main so the exact same steps run everywhere, avoiding drift between what a PR tests and what actually ships.
  3. Artifacts carry the build forward

    dist/** declared as an artifactsentry in the first step makes the compiled output available to the deployment step's fresh container, which otherwise starts empty.
  4. Deployment step is tagged to an environment

    deployment: staging associates the step with the Staging environment in Deployments, giving it its own audit history and, optionally, its own restricted variables.
Section 10

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.

bitbucket-pipelines.yml
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"
  1. Engineer opens a pull request

    The pull-requests pipeline runs automatically and posts a build status onto the PR; a required merge check blocks the merge button until it passes.
  2. Reviewer approves and merges

    Branch permissions on main require at least one approval and a green build — both are satisfied before the merge is even allowed.
  3. Merge triggers the main pipeline

    The merge commit lands on main, matching pipelines.branches.main, which rebuilds and produces the dist/ artifact.
  4. Deployment step ships to staging over SSH

    rsync copies the build output to the staging host using the Pipelines-managed SSH key, then a remote systemctl restart picks up the new code.
  5. Deployment history is recorded

    Because the step is tagged deployment: staging, it shows up under Deployments → Staging with a timestamp, commit, and status — giving a clear audit trail of what is currently live.
Troubleshooting

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's authorized_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 validates bitbucket-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 under pipelines.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

Best Practices

  • Reuse step definitions with YAML anchors (&name / *name) so PR checks and production builds run identical steps.
  • Always declare artifacts explicitly — never assume a later step can see a previous step's filesystem.
  • Use caches for 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 like latest, so a base image update can't silently break builds.
Security

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

Interview Questions

It's the CI/CD configuration file for Bitbucket Pipelines, committed at the root of the repository. Bitbucket reads it on every push or pull request event and runs any pipeline whose trigger rule matches.
Cheat Sheet

Cheat Sheet

bitbucket-cheatsheet.yml
# --- 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.0
Summary

Summary

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

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