CircleCI jobs run inside Docker containers. This is the core of how CircleCI provides isolated, reproducible, and consistent build environments.

Here’s how it looks in action. When you define a job in your .circleci/config.yml, you specify a Docker image. This image becomes the environment where your commands execute.

version: 2.1

jobs:
  build-and-test:
    docker:
      - image: cimg/node:18.17.1
    steps:
      - checkout
      - run:
          name: Install Dependencies
          command: npm install
      - run:
          name: Run Tests
          command: npm test

In this example, the build-and-test job will execute within a Docker container based on the cimg/node:18.17.1 image. CircleCI pulls this image, starts a container from it, and then runs your steps inside that container. This means your build process has access to everything pre-installed in that Node.js image, like npm and node.

The primary problem CircleCI’s Docker execution solves is environment drift. Without it, developers would run builds on their local machines, which inevitably have different versions of dependencies, operating systems, and tools. This leads to the classic "it works on my machine" problem. By specifying a Docker image, you ensure that every build, regardless of who triggers it or where the CircleCI runner is located, starts with the exact same foundation.

Internally, CircleCI orchestrates this by using Docker’s containerization technology. It manages a pool of Docker daemons. When a job is scheduled, CircleCI selects an available runner, instructs its Docker daemon to pull the specified image (if not already present), and then starts a container. It mounts your repository code into the container, typically at /opt/repo, and executes your job’s commands within that container’s isolated filesystem and network.

You have several levers to control this execution environment.

First, the image key is paramount. You can use official images (like ubuntu:22.04, python:3.10-slim), community images (like node:18), or even your own custom-built Docker images hosted on a registry like Docker Hub or AWS ECR.

jobs:
  deploy-app:
    docker:
      - image: 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-custom-app:latest

Second, you can define multiple services using the services key. This is incredibly useful for running databases or other dependencies your application needs during tests. These services also run as Docker containers, linked to your primary job container.

jobs:
  test-with-db:
    docker:
      - image: cimg/go:1.20
    services:
      - image: postgres:14
        environment:
          POSTGRES_USER: testuser
          POSTGRES_DB: testdb
    steps:
      - checkout
      - run:
          name: Run migrations and tests
          command: |
            echo "Waiting for database..."
            # Add logic here to wait for Postgres to be ready
            go run ./cmd/migrate
            go test ./...

Here, a PostgreSQL container starts alongside your Go job container. Your job container can then connect to this PostgreSQL service, typically via localhost on the default PostgreSQL port (5432).

You can also push and pull Docker layers, which can speed up builds if you frequently use custom images or want to cache intermediate build artifacts within Docker. This involves using docker:push and docker:pull steps within your job.

The most surprising thing about CircleCI’s Docker integration is how seamlessly it handles networking between your primary job container and any defined services. When you declare a service, CircleCI automatically configures it so that your primary job container can access it. For instance, if you define a postgres:14 service, your job container can connect to it using localhost:5432. You don’t need to manually configure IP addresses or network bridging; CircleCI abstracts this complexity away, making it feel like the services are running directly within your job’s environment, even though they are distinct containers.

The next step in understanding CircleCI’s execution environments involves exploring custom Docker images and how to build them effectively for CI/CD.

Want structured learning?

Take the full Circleci course →