The CircleCI Machine Executor lets you ditch containerized builds for full virtual machine access, which sounds like a step backward but is actually a massive leap for certain kinds of CI/CD.
Let’s see it in action. Imagine you need to build a Docker image, but not on CircleCI – you need to build it locally on a machine that’s controlled by CircleCI, and then push it to a registry. Or maybe you need to install kernel modules, or run tests that require specific hardware access. This is where the Machine Executor shines.
Here’s a sample .circleci/config.yml demonstrating its use:
version: 2.1
executors:
machine-executor:
docker:
image: cimg/base:stable
resource_class: large
working_directory: /home/circleci/project
jobs:
build-and-push-docker:
executor: machine-executor
steps:
- checkout
- run:
name: Install Docker
command: |
sudo apt-get update
sudo apt-get install -y docker.io
sudo systemctl start docker
sudo usermod -aG docker circleci
- run:
name: Build Docker Image
command: docker build -t my-docker-repo/my-app:latest .
- run:
name: Push Docker Image
command: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin && docker push my-docker-repo/my-app:latest
In this example, we’re defining a machine-executor that uses a standard Ubuntu base image (cimg/base:stable) and allocates a large resource class. The jobs section then uses this executor. The key difference is that the run steps here execute directly on the virtual machine, not within a container. We install Docker, build an image, and then push it. This is something you’d struggle to do efficiently with the Docker Executor, which runs your build inside a Docker container.
The problem this solves is simple: not all build and test processes fit neatly into the isolated, ephemeral world of containers. Some require direct hardware access, the ability to manipulate system-level services (like starting docker.io itself), or the installation of low-level dependencies that might conflict with a containerized environment. The Machine Executor provides a full Linux VM for each job, giving you the same flexibility you’d have on a dedicated build server.
Internally, CircleCI provisions a dedicated virtual machine for each job that uses a Machine Executor. This VM is fully accessible via sudo, allowing you to install any software, configure network interfaces, manage services, and perform operations that are impossible within the restricted environment of a Docker container. When the job finishes, the VM is deprovisioned. This provides a clean slate for every build while offering the necessary power and flexibility.
The primary levers you control are the resource_class and the docker.image in your executor definition. The resource_class dictates the CPU, RAM, and disk available to your VM (e.g., small, medium, large, xlarge, 2xlarge). The docker.image is still relevant; it defines the initial state of the VM’s filesystem. You’ll typically use one of the cimg/base images as a starting point, which provides a clean Ubuntu environment.
A common pitfall is forgetting that the working_directory for the Machine Executor is still a path on the VM. This means that if you check out code, it lands in /home/circleci/project on that VM, and subsequent commands operate from there. If you need to clean up artifacts between jobs, you might need to explicitly rm -rf directories on the VM itself, as there’s no automatic container cleanup.
The next step after mastering the Machine Executor is often exploring how to manage state persistence across builds, which can be tricky given the ephemeral nature of VMs.