How do you configure Bitbucket Pipelines for Docker-based builds including building, testing, and pushing container images?
Quick Answer
Bitbucket Pipelines supports Docker-in-Docker by enabling the 'docker' service in pipeline steps, allowing you to build Docker images, run container-based tests, and push images to registries like Docker Hub, AWS ECR, or GCR. Docker layer caching and multi-stage builds optimize image build performance.
Detailed Answer
Building Docker images inside a pipeline is like running a factory inside another factory. The outer factory (Bitbucket Pipeline step) provides the workspace, and the inner factory (Docker daemon) assembles the products (container images). The trick is that the inner factory needs special privileges to operate, which is why Bitbucket requires you to explicitly enable the Docker service.
Bitbucket Pipelines runs each step inside a Docker container, but by default, Docker commands are not available within that container. To build Docker images, you must add 'docker' to the step's 'services' list. This enables Docker-in-Docker (DinD) by mounting the host's Docker socket into the step container. The step then has access to docker build, docker push, and other Docker CLI commands. The Docker service consumes its own memory allocation (1 GB default), which is separate from the step's memory. This means a step with 'size: 2x' and Docker service uses 2 GB for the step plus 1 GB for Docker, totaling 3 GB.
Docker layer caching in Bitbucket Pipelines works through the 'docker' cache defined in the pipeline. When enabled, Docker saves its layer cache at the end of the step and restores it at the beginning of the next run. This dramatically speeds up builds for images that change infrequently (base layers, dependency installation) while only rebuilding the layers that changed (application code). Multi-stage Docker builds complement this by separating build dependencies from the runtime image, producing smaller final images and allowing parallel cache restoration.
In production, the typical Docker CI/CD pipeline includes three phases: build the image, test it, and push it. The build step uses docker build with appropriate tags (commit hash for immutability, 'latest' for convenience). The test step runs the image with docker run to execute integration tests against the containerized application. The push step authenticates with the container registry (Docker Hub, AWS ECR, GCR) and pushes the tagged images. Teams often use Bitbucket Pipes for the push step (e.g., atlassian/aws-ecr-push-image) to handle authentication and tagging automatically.
A gotcha that causes flaky builds: the Docker layer cache in Bitbucket is not guaranteed. If the cache is evicted (due to size limits or 7-day TTL), the next build starts from scratch, which can double the build time. Teams relying on cache for fast builds should also optimize their Dockerfiles with proper layer ordering (dependencies before source code) and use multi-stage builds to minimize the impact of cache misses. Another common mistake is running out of disk space when building large images; the pipeline step has limited disk, and layers from previous builds can fill it up. Using --no-cache selectively for certain stages or pruning intermediate images can prevent this.