In CircleCI, developers can freely combine images like building blocks to create containerized continuous integration (CI) environments. CircleCI supports Docker natively, allowing you to build, test, and deploy your application as a Docker image.
In this post, you will learn how to build a Docker image in CircleCI and push the image to Docker Hub, Docker’s official cloud-based registry for Docker images.
Prerequisites
- A CircleCI account
- A GitHub account
- A Docker Hub account
Setting up the pipeline configuration
In your project’s root directory, create a config.yml
in a directory called .circleci
:
version: 2.1
jobs:
build:
working_directory: ~/app
docker:
- image: cimg/base:2022.09
auth:
username: $DOCKERHUB_USERNAME
password: $DOCKERHUB_PASSWORD
steps:
- checkout
- setup_remote_docker:
docker_layer_caching: true
- run:
name: Run tests
command: |
docker-compose -f ./docker-compose.test.yml up
- run:
name: Build Docker image
command: |
TAG=0.1.$CIRCLE_BUILD_NUM
docker build -t $DOCKERHUB_USERNAME/circleci-new-docker-example:$TAG .
- run:
name: Push application Docker image
command: |
TAG=0.1.$CIRCLE_BUILD_NUM
echo $DOCKERHUB_PASSWORD | docker login -u $DOCKERHUB_USERNAME --password-stdin
docker push $DOCKERHUB_USERNAME/circleci-new-docker-example:$TAG
Depending on the project, some of the details may change. If you want to try these steps for yourself, I prepared a sample project that you can use this config file with.
The project is written in Node.js using Express and simply returns “Hello World”, tested with Jest and supertest.
In the following sections, we will explore how the CircleCI configuration works to build, test, and publish a Docker image of the sample project.
Exploring the configuration structure
version: 2.1
jobs:
build:
docker: ...
steps: ...
In CircleCI, you can define the steps
of each job, including repository checkout and cache-related processing. This gives you great freedom, not only for defining the CI container environment but also for the automated steps in your CI/CD pipeline. You can find all the details in the official documentation.
Using the Docker executor
docker:
- image: cimg/base:2022.09
In this section, you define a cimg/base
Docker image as your execution environment. You can use this pre-built convenience image to set up the CI build environment, Ubuntu in our case. This image is also very useful as a base for any custom Docker images.
Your goal is to have an execution environment with Docker and Git installed, both of which are included in cimg/base
. As stated in the official documentation, this image contains all you need to run most builds, including Git, Docker, Docker Compose, and much more.
Accessing the Docker image registry
docker:
- image: ...
auth:
username: $DOCKERHUB_USERNAME
password: $DOCKERHUB_PASSWORD
The auth
field is used to specify the credentials needed to access the Docker Hub image registry. These credentials are pulled in as environment variables, which you can set either at the project level or in an organization-wide context. To learn more, read the documentation on setting an environment variable.
Checkout
steps:
- checkout
The first step, checkout
, is a special step to check out the source code of your application. This will be downloaded to the working directory in CircleCI.
Set up remote Docker and Docker layer caching
- setup_remote_docker:
docker_layer_caching: true
This step helps you avoid the Docker-in-Docker problem. In fact, you’re setting up an environment that is isolated from the CI or primary container environment, then using the remote host’s Docker Engine.
The setup_remote_docker
key configures your remote Docker environment. By setting up a remote Docker environment, you can run Docker commands, such as building and pushing Docker images, within your job.
The docker_layer_caching
option for your remote environment is set to true
, which enables Docker layer caching. With Docker layer caching enabled, CircleCI can reuse layers from previous builds if they haven’t changed. This significantly reduces build times, as unchanged layers do not need to be rebuilt.
Docker layer caching is included in every CircleCI plan, including the free plan. You can refer to the sample project’s build history and compare this workflow where Docker layer caching was enabled with the duration here without Docker layer caching. There we noticed an improvement of more than 30 seconds in the jobs with DLC enabled.
Running tests
- run:
name: Run tests
command: |
docker-compose -f ./docker-compose.test.yml up
For this project I am running the tests with Docker Compose, and the tests are run only in the application container. If you need a database container, it is easy to set one up using Docker Compose.
Pushing the Docker image
- run:
name: Build and Push application Docker image
command: |
TAG=0.1.$CIRCLE_BUILD_NUM
docker build -t $DOCKERHUB_USERNAME/circleci-docker-example:$TAG .
echo $DOCKERHUB_PASSWORD | docker login -u $DOCKERHUB_USERNAME --password-stdin
docker push $DOCKERHUB_USERNAME/circleci-docker-example:$TAG
This is where the application’s Docker image is built and pushed to Docker Hub. In a chronological order, it:
- Creates a Docker image tag using the built-in environment variable
CIRCLE_BUILD_NUM
, which provides a unique build number for each run - Builds a Docker image from the Dockerfile in the current directory (
.
) - Tags the image with the Docker Hub username and the generated tag
- Reads a password from the CircleCI environment variable and passes it to the
docker login
command for authentication purposes. - Pushes the image to Docker Hub
Conclusion
CircleCI provides two ways to build a Docker image: using a dedicated virtual machine with the machine executor or using the remote Docker engine with a Docker executor. In this post, you learned how to build your Docker images in the Docker execution environment.
By automating the build, test, and deploy workflow for your Docker images with CircleCI, you can significantly speed up build times and ensure consistent, reliable deployments. To get started, sign up for a free CircleCI account today.
Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest in solving day-to-day problems encountered by users, he ventured into programming and has since directed his problem solving skills at building software for both web and mobile. A full stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and blog posts on several blogs around the world. Being tech savvy, his hobbies include trying out new programming languages and frameworks.