Introduction
CircleCI is a cloud-native CI/CD platform that has been a mainstay of the developer toolchain since 2011. Built from the ground up for speed and developer experience, CircleCI differentiated itself through Docker-first execution, generous parallelism options, and a powerful caching system that can cut build times by 50-80%.
While GitHub Actions has captured the "CI adjacent to code" market, CircleCI remains popular for teams that need advanced optimization features — particularly Docker Layer Caching, intelligent test splitting, and granular resource class selection. Organizations with large test suites and complex Docker builds often find CircleCI's purpose-built features deliver significantly faster pipelines.
Market Positioning
CircleCI occupies the "premium CI" space — more expensive than GitHub Actions' free tier but offering performance features that justify the cost for engineering-heavy organizations. Its credit-based pricing model charges for actual compute usage rather than flat per-user fees, making it economical for teams with sporadic build patterns and expensive for teams running constant heavy workloads.
Configuration Basics
CircleCI configuration lives in .circleci/config.yml at the repository root. Version 2.1 (current) supports orbs, reusable executors, parameterized jobs, and pipeline parameters.
# .circleci/config.yml
version: 2.1
orbs:
node: circleci/node@5.2.0
docker: circleci/docker@2.6.0
executors:
app-executor:
docker:
- image: cimg/node:20.11
- image: cimg/postgres:16.1
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
resource_class: medium
working_directory: ~/project
jobs:
install-deps:
executor: app-executor
steps:
- checkout
- node/install-packages:
pkg-manager: npm
cache-path: node_modules
- persist_to_workspace:
root: ~/project
paths:
- node_modules
lint:
executor: app-executor
steps:
- checkout
- attach_workspace:
at: ~/project
- run:
name: Run ESLint
command: npm run lint
test:
executor: app-executor
parallelism: 4
steps:
- checkout
- attach_workspace:
at: ~/project
- run:
name: Run tests with splitting
command: |
TESTFILES=$(circleci tests glob "tests/**/*.test.js" | circleci tests split --split-by=timings)
npm test -- $TESTFILES
- store_test_results:
path: test-results
- store_artifacts:
path: coverage
build-image:
executor: docker/docker
steps:
- setup_remote_docker:
docker_layer_caching: true
- checkout
- docker/build:
image: myorg/myapp
tag: ${CIRCLE_SHA1}
- docker/push:
image: myorg/myapp
tag: ${CIRCLE_SHA1}
workflows:
build-test-deploy:
jobs:
- install-deps
- lint:
requires: [install-deps]
- test:
requires: [install-deps]
- build-image:
requires: [lint, test]
filters:
branches:
only: main
Key concepts in the configuration:
- Orbs — Reusable configuration packages (like GitHub Actions marketplace actions)
- Executors — Named execution environments with Docker images, resource classes, and environment variables
- Jobs — Collections of steps that run on a single executor
- Workflows — Orchestrate jobs with dependencies, filters, and scheduling
- Workspaces — Persist and share data between jobs in a workflow
Executors
CircleCI offers four executor types, each optimized for different workloads. Choosing the right executor significantly impacts both speed and cost.
Docker: Most common. Runs steps inside a Docker container. Supports multiple service containers (databases, caches). Fast startup (~5s). Cannot run Docker-in-Docker without setup_remote_docker.
Machine: Full Linux VM. Required for Docker builds without DLC, privileged operations, and full networking control. Slower startup (~30-60s). Supports Docker natively.
macOS: macOS VMs for iOS/macOS builds. Xcode pre-installed. Most expensive resource class. Required for Apple-specific toolchains.
Windows: Windows Server VMs. Visual Studio Build Tools, .NET Framework pre-installed. Required for Windows-native builds.
# Docker executor with service containers
executors:
integration-test:
docker:
- image: cimg/python:3.12 # Primary container
- image: cimg/redis:7.2 # Service container
- image: cimg/postgres:16.1
environment:
POSTGRES_DB: app_test
POSTGRES_USER: test
POSTGRES_PASSWORD: secret
resource_class: large # 4 vCPU, 8GB RAM
environment:
DATABASE_URL: postgresql://test:secret@localhost:5432/app_test
REDIS_URL: redis://localhost:6379
# Machine executor for Docker builds
executors:
docker-builder:
machine:
image: ubuntu-2404:current
resource_class: medium # 2 vCPU, 7.5GB RAM
# ARM executor
executors:
arm-builder:
machine:
image: ubuntu-2404:current
resource_class: arm.medium # ARM-based 2 vCPU
Resource classes determine CPU and memory allocation. CircleCI charges credits per minute based on the resource class:
| Resource Class | vCPU | RAM | Credits/min |
|---|---|---|---|
| small | 1 | 2 GB | 5 |
| medium | 2 | 4 GB | 10 |
| medium+ | 3 | 6 GB | 15 |
| large | 4 | 8 GB | 20 |
| xlarge | 8 | 16 GB | 40 |
| 2xlarge | 16 | 32 GB | 80 |
| 2xlarge+ | 20 | 40 GB | 100 |
Orbs
Orbs are CircleCI's reusable configuration packages — shareable bundles of jobs, commands, and executors. They're the equivalent of GitHub Actions marketplace actions but with deeper integration into CircleCI's config system.
# Using community orbs
version: 2.1
orbs:
aws-cli: circleci/aws-cli@4.1.3
slack: circleci/slack@4.13.3
docker: circleci/docker@2.6.0
node: circleci/node@5.2.0
terraform: circleci/terraform@3.3.0
jobs:
deploy:
executor: aws-cli/default
steps:
- checkout
- aws-cli/setup:
role_arn: arn:aws:iam::123456789:role/ci-deploy
- run:
name: Deploy to ECS
command: |
aws ecs update-service \
--cluster production \
--service my-app \
--force-new-deployment
- slack/notify:
event: pass
template: basic_success_1
# Creating a custom orb (orb.yml)
version: 2.1
description: |
Custom orb for deploying to our Kubernetes clusters.
executors:
k8s-deployer:
docker:
- image: bitnami/kubectl:1.29
commands:
deploy:
parameters:
cluster:
type: string
namespace:
type: string
default: default
manifest:
type: string
steps:
- run:
name: Configure kubectl
command: |
echo "$KUBE_CONFIG" | base64 -d > ~/.kube/config
- run:
name: Apply manifests
command: |
kubectl apply -f << parameters.manifest >> \
--namespace << parameters.namespace >> \
--context << parameters.cluster >>
- run:
name: Verify rollout
command: |
kubectl rollout status deployment/app \
--namespace << parameters.namespace >> \
--timeout=300s
jobs:
deploy-to-cluster:
executor: k8s-deployer
parameters:
cluster:
type: string
namespace:
type: string
steps:
- checkout
- deploy:
cluster: << parameters.cluster >>
namespace: << parameters.namespace >>
manifest: kubernetes/
Workflows
Workflows orchestrate job execution with dependencies, parallel paths, approval gates, and branch/tag filtering. They provide visual pipeline representation in the CircleCI UI.
flowchart TD
INSTALL[install-deps] --> LINT[lint]
INSTALL --> UNIT[unit-test]
INSTALL --> INTEG[integration-test]
INSTALL --> SEC[security-scan]
LINT --> BUILD[build-image]
UNIT --> BUILD
INTEG --> BUILD
SEC --> BUILD
BUILD --> STAGING[deploy-staging]
STAGING --> APPROVE{Manual Approval}
APPROVE --> PROD[deploy-production]
style INSTALL fill:#3B9797,color:#fff
style BUILD fill:#132440,color:#fff
style APPROVE fill:#BF092F,color:#fff
style PROD fill:#132440,color:#fff
# Complex workflow with fan-out/fan-in and approval
workflows:
build-test-deploy:
jobs:
# Fan-out: parallel jobs after install
- install-deps
- lint:
requires: [install-deps]
- unit-test:
requires: [install-deps]
- integration-test:
requires: [install-deps]
- security-scan:
requires: [install-deps]
# Fan-in: build requires all checks
- build-image:
requires: [lint, unit-test, integration-test, security-scan]
filters:
branches:
only: [main, release/*]
# Deploy staging automatically
- deploy-staging:
requires: [build-image]
context: staging-context
# Manual approval gate
- approve-production:
type: approval
requires: [deploy-staging]
# Deploy production after approval
- deploy-production:
requires: [approve-production]
context: production-context
# Scheduled workflow (nightly builds)
nightly-full-test:
triggers:
- schedule:
cron: "0 2 * * *"
filters:
branches:
only: main
jobs:
- full-integration-suite
- performance-tests
- security-audit
Docker Layer Caching
Docker Layer Caching (DLC) is CircleCI's premium feature that persists Docker build layers between pipeline runs. For Dockerfiles that haven't changed in early layers (OS packages, dependencies), DLC can reduce build times from 10+ minutes to under 1 minute.
# Enable DLC with setup_remote_docker
jobs:
build-docker:
docker:
- image: cimg/base:current
steps:
- checkout
- setup_remote_docker:
docker_layer_caching: true # Enables DLC (costs 200 credits/run)
- run:
name: Build Docker image
command: |
docker build -t myapp:${CIRCLE_SHA1} .
docker push myregistry/myapp:${CIRCLE_SHA1}
# Machine executor with DLC
build-docker-machine:
machine:
image: ubuntu-2404:current
docker_layer_caching: true # DLC on machine executor
steps:
- checkout
- run:
name: Build multi-arch image
command: |
docker buildx create --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag myregistry/myapp:${CIRCLE_SHA1} \
--push .
Test Splitting & Parallelism
CircleCI's test splitting distributes test files across parallel containers to minimize total test time. The circleci tests split command supports three strategies: by filename, by filesize, and by timing data (most effective).
# Test splitting with parallelism
jobs:
test:
docker:
- image: cimg/python:3.12
parallelism: 8 # Run 8 parallel containers
steps:
- checkout
- run:
name: Install dependencies
command: pip install -r requirements.txt
- run:
name: Run tests with time-based splitting
command: |
# Glob test files
TESTFILES=$(circleci tests glob "tests/**/test_*.py" | \
circleci tests split --split-by=timings)
# Run only this container's portion
pytest $TESTFILES \
--junitxml=test-results/junit.xml \
--cov=src \
--cov-report=xml
- store_test_results:
path: test-results # Feeds timing data back to CircleCI
- store_artifacts:
path: coverage
# JavaScript test splitting with Jest
jobs:
test-js:
docker:
- image: cimg/node:20.11
parallelism: 4
steps:
- checkout
- run: npm ci
- run:
name: Run Jest with splitting
command: |
TESTFILES=$(circleci tests glob "src/**/*.test.{js,ts}" | \
circleci tests split --split-by=timings)
npx jest $TESTFILES \
--ci \
--reporters=default \
--reporters=jest-junit
environment:
JEST_JUNIT_OUTPUT_DIR: test-results
- store_test_results:
path: test-results
store_test_results uploads JUnit XML data, subsequent runs use actual test durations for optimal distribution. Timing data is retained for 30 days.
Caching
CircleCI's caching system persists files between pipeline runs — primarily for dependency installation (node_modules, pip packages, Maven artifacts). Effective caching can reduce a 3-minute npm install to a 5-second cache restore.
# Dependency caching with fallback keys
jobs:
build:
docker:
- image: cimg/node:20.11
steps:
- checkout
- restore_cache:
keys:
# Exact match (lockfile unchanged)
- v2-deps-{{ checksum "package-lock.json" }}
# Partial match (some deps changed)
- v2-deps-
- run: npm ci
- save_cache:
key: v2-deps-{{ checksum "package-lock.json" }}
paths:
- node_modules
- ~/.npm
# Multi-language caching
jobs:
build-monorepo:
docker:
- image: cimg/base:current
steps:
- checkout
# Python dependencies
- restore_cache:
keys:
- v1-pip-{{ checksum "backend/requirements.txt" }}
- v1-pip-
- run: pip install -r backend/requirements.txt
- save_cache:
key: v1-pip-{{ checksum "backend/requirements.txt" }}
paths:
- ~/.cache/pip
# Go modules
- restore_cache:
keys:
- v1-gomod-{{ checksum "services/go.sum" }}
- v1-gomod-
- run: cd services && go mod download
- save_cache:
key: v1-gomod-{{ checksum "services/go.sum" }}
paths:
- ~/go/pkg/mod
Contexts & Secrets
Contexts provide organization-level secret management. Unlike project-level environment variables, contexts can be shared across multiple projects and restricted to specific security groups.
# Using contexts in workflows
workflows:
deploy-pipeline:
jobs:
- build
- deploy-staging:
requires: [build]
context:
- aws-staging # AWS credentials for staging
- slack-notify # Slack webhook URL
- deploy-production:
requires: [deploy-staging]
context:
- aws-production # Different AWS credentials for production
- slack-notify
filters:
branches:
only: main
Context security features include: restricting context access to specific GitHub teams/groups, requiring approval to use a context, and audit logging of all context usage. Environment variables within a context are masked in build output automatically.
Pipeline Parameters
Pipeline parameters enable dynamic configuration — conditional workflows based on parameters, triggered via API or UI. Combined with setup workflows, they enable monorepo path filtering and dynamic pipeline generation.
# Pipeline parameters for conditional workflows
version: 2.1
parameters:
run-integration-tests:
type: boolean
default: false
deploy-env:
type: enum
enum: [staging, production]
default: staging
service:
type: string
default: ""
jobs:
deploy:
docker:
- image: cimg/base:current
steps:
- run:
name: Deploy to << pipeline.parameters.deploy-env >>
command: |
echo "Deploying << pipeline.parameters.service >> to << pipeline.parameters.deploy-env >>"
workflows:
standard:
when:
not: << pipeline.parameters.run-integration-tests >>
jobs:
- build
- test
- deploy:
requires: [test]
filters:
branches:
only: main
integration:
when: << pipeline.parameters.run-integration-tests >>
jobs:
- full-integration-suite
# Trigger pipeline with parameters via API
curl -X POST https://circleci.com/api/v2/project/gh/myorg/myrepo/pipeline \
-H "Circle-Token: $CIRCLECI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"branch": "main",
"parameters": {
"run-integration-tests": true,
"deploy-env": "staging",
"service": "backend"
}
}'
Self-Hosted Runners
CircleCI runners let you execute jobs on your own infrastructure — required for accessing private networks, specialized hardware (GPUs), or compliance-restricted environments. Runners support Linux, macOS, Windows, and container-based execution.
# Using self-hosted runner
jobs:
deploy-internal:
machine: true
resource_class: myorg/private-network-runner # Self-hosted runner class
steps:
- checkout
- run:
name: Deploy to internal Kubernetes
command: |
kubectl apply -f manifests/ --context internal-cluster
- run:
name: Run smoke tests against internal services
command: |
curl -f http://internal-app.corp.local/health
# Install CircleCI runner on Linux
# 1. Create resource class in CircleCI UI (Organization Settings > Self-Hosted Runners)
# 2. Install runner agent on your machine
curl -s https://raw.githubusercontent.com/CircleCI-Public/runner-installation-files/main/download-launch-agent.sh | bash
# Configure runner
cat > /etc/circleci-runner/circleci-runner-config.yaml <<EOF
api:
auth_token: YOUR_RUNNER_TOKEN
runner:
name: my-runner-01
working_directory: /var/lib/circleci-runner/workdir
cleanup_working_directory: true
EOF
# Start runner service
systemctl enable circleci-runner
systemctl start circleci-runner
Insights & Optimization
CircleCI Insights provides analytics on pipeline performance — helping identify slow jobs, flaky tests, and credit usage patterns. Key optimization strategies:
flowchart TD
START[Slow Pipeline?] --> Q1{Where is time spent?}
Q1 -->|Dependencies| C1[Optimize caching]
Q1 -->|Tests| C2[Enable parallelism + splitting]
Q1 -->|Docker builds| C3[Enable DLC]
Q1 -->|Waiting| C4[Reduce job dependencies]
C1 --> R1[Use checksum keys + fallbacks]
C2 --> R2[Set parallelism: 4-8 + timings split]
C3 --> R3[Analyze cache hit rate first]
C4 --> R4[Fan-out independent jobs]
style START fill:#BF092F,color:#fff
style C1 fill:#3B9797,color:#fff
style C2 fill:#3B9797,color:#fff
style C3 fill:#3B9797,color:#fff
style C4 fill:#3B9797,color:#fff
- Right-size resource classes — Don't use
xlargefor jobs that only needsmall. Monitor CPU/memory utilization in Insights. - Parallelize aggressively — Test splitting with 4-8 containers often provides the best cost/speed tradeoff.
- Workspace vs cache — Use workspaces for passing data within a single workflow run. Use caches for data that persists across runs (dependencies).
- Filter early — Use path filtering to skip jobs that don't need to run. Fail fast with lint before expensive tests.
- Scheduled workflows — Run expensive operations (security scans, full integration suites) on schedule rather than every commit.
Exercises
Create a CircleCI config for a Node.js application with: npm dependency caching (with checksum-based keys and fallback), ESLint in a separate job, Jest tests split across 4 parallel containers with timing-based splitting, and Docker image build with DLC. The full pipeline should complete in under 3 minutes for a 500-test suite.
Create a custom orb that encapsulates your team's deployment process. Include: a reusable executor, parameterized commands for building and deploying, and a complete job that teams can use with minimal configuration. Publish the orb to your organization's namespace and consume it from a project config.
Implement a setup workflow for a monorepo with 3 services. The setup workflow should detect which directories changed (using git diff), set pipeline parameters accordingly, and trigger only the relevant service pipelines. Verify that changing backend/ only runs backend jobs.
Take an existing CircleCI pipeline and optimize it for credit efficiency. Analyze: which jobs use oversized resource classes, where caching could reduce install times, whether DLC cache hit rate justifies its cost, and where parallelism provides diminishing returns. Target a 40% credit reduction while maintaining or improving total pipeline duration.