---
title: "Fast Tests, Tiny Docker Image"
url: "https://www.cypress.io/blog/fast-tests-tiny-docker-image"
description: "Use multi-stage Docker builds to install Cypress and run end-to-end tests, while keeping the production image tiny."
---

# Fast Tests, Tiny Docker Image

August 8, 2018

By Gleb Bahmutov

![An abstract image of different shades and colors](https://storage.ghost.io/c/12/89/12899ca1-4786-4eea-b7e1-9c5268baa48a/content/images/2025/04/Photo-1527148191484-5472x3648-1.jpeg)

If the Cypress Test Runner were a person, its best friend would be a person named Docker. Really, Cypress and Docker work so well together! For example, all our CI builds are using [cypress-docker-images](https://github.com/cypress-io/cypress-docker-images?ref=cypress-io.ghost.io) to include all necessary dependencies in order to successfully install and run Cypress tests. Just run `npm ci` and `cypress run` and you are good to go.

```docker
FROM cypress/base:10
RUN npm install
# or even better: RUN npm ci
RUN $(npm bin)/cypress run
```

We use this approach to run a lot of tests and examples ourselves, mostly on [CircleCI](https://circleci.com/?ref=cypress-io.ghost.io), and we have detailed documentation for various other [CIs](https://on.cypress.io/continuous-integration?ref=cypress-io.ghost.io).

We use this approach to run a lot of tests and examples ourselves, mostly on [CircleCI](https://circleci.com/?ref=cypress-io.ghost.io), and we have detailed documentation for various other [CIs](https://on.cypress.io/continuous-integration?ref=cypress-io.ghost.io).

In this blog post I will show you how to use [Docker multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/?ref=cypress-io.ghost.io) to keep the _production_ image size minimal while still running end-to-end tests using Cypress.

**Note:** you can find the complete source code for this blog post in [this repo](https://github.com/bahmutov/tiny-blog?ref=cypress-io.ghost.io).

## Multi-stage builds

I have described building several Docker images from a single Dockerfile in the blog post - [Making small Docker image](https://glebbahmutov.com/blog/making-small-docker-image/?ref=cypress-io.ghost.io). In a nutshell, we are going to describe how to build _several_ images and even copy some files from one image to another image to make sure we are using already tested files in production.

```docker
# first image will be our test image and will include Cypress
# and any end-to-end tests
FROM cypress/base:10 as TEST
COPY package.json .
RUN npm install
RUN npm test
...
# this is our output "production" image
# without any test dependencies
FROM busybox as PROD
# for example we can copy some files from TEST image to this production image
COPY --from=TEST /app/public /public
...
```

By making two images we can avoid installing dev dependencies in the production image, which keeps its size very small.

## Caching is the hard problem

When designing a Docker image to run tests, we must carefully consider how to cache files, because this affects how and whether the Docker build will [run individual commands](https://docs.docker.com/v17.09/engine/userguide/eng-image/dockerfile_best-practices/?ref=cypress-io.ghost.io#build-cache). What we really want during the `docker build .` execution:

-   Only re-install NPM dependencies if the `package.json` (or `package-lock.json`) file changes
-   Re-run the Cypress tests only if our spec files or the source files change

Here is the simplest Dockerfile:

```docker
FROM cypress/base:10 as TEST
WORKDIR /app
# dependencies will be installed only if the package.json file changes
COPY package.json .
RUN npm install
# copy spec files and website files
COPY cypress cypress
COPY cypress.json .
COPY public public
# rerun E2E tests only if any of the previous files change
RUN npm test
```

You can play with this setup by changing source files and rerunning `docker build .`. Depending on the file changed you should see different outputs:

-   The first time you build the image, it will install Cypress and will execute the E2E tests
-   Any time you change `package.json` it will re-install Cypress and will execute the E2E tests
-   If you only change spec files inside the `cypress/integration` folder, the `npm install` command will be skipped because the `package.json` file has not changed, and the Docker `build` command is smart to pull a cached layer image. Only `RUN npm test` will be executed
-   If nothing changes, no commands will be run - and `docker build .` will finish quickly.

## Always running E2E tests

You might want to _always_ run E2E tests, even if the spec files have not changed. This is a very common case if you are testing an external site. In this case you will run into the [Docker cache busting problem](https://github.com/moby/moby/issues/1996?ref=cypress-io.ghost.io) which only has hacky solutions 😖. I like defining a build argument before the command to bust on demand, and passing a new value whenever I want to rerun it.

```docker
ARG BUST=1
# if you run "docker build . --build-arg BUST=foo"
# it will bust this cache and it will rerun all commands from here
RUN npm test
```

To always rerun the `npm test` command I need to pass a new value to the `BUST` argument which I can do by using a timestamp

```text
docker build . --build-arg BUST=$(date +%s)
```

Imperfect, but works.

## Size savings

So how big of a size savings are we talking about? Is the complexity of multi-stage build worth it? We are using a very small [busybox](https://hub.docker.com/_/busybox/?ref=cypress-io.ghost.io) image to statically serve the `public` folder. Compared to `cypress/base:10` + NPM dev dependencies (which includes unzipped Cypress) it is _**tiny**_.

```text
docker images
REPOSITORY             TAG        IMAGE ID            CREATED             SIZE
none                   none       5271d608f7b2        About an hour ago   1.35GB
cypress/base           10         1613db8573fa        3 months ago        926MB
busybox                latest     22c2dd5ee85d        2 weeks ago         1.16MB
```

Holy moly!

Not only does `cypress/base:10` weigh almost 1000x more than busy box, installing NPM dependencies and the Cypress binary adds another 400MB!

Here are the layers of the built `TEST` image to see where the megabytes are coming from

```text
$ docker history 5271d608f7b2
IMAGE               CREATED             CREATED BY                                      SIZE
5271d608f7b2        About an hour ago   |1 HOSTNAME=1533380839 /bin/sh -c npm test      5.48MB
e4e87ef3eb74        About an hour ago   /bin/sh -c #(nop)  ARG HOSTNAME=1               0B
2805214ce399        About an hour ago   /bin/sh -c ls -la public                        0B
0f221e552006        About an hour ago   /bin/sh -c ls -la                               0B
aae6148fa25a        About an hour ago   /bin/sh -c #(nop) COPY dir:7b879359721ac4fd1…   70B
15c188cccb9a        About an hour ago   /bin/sh -c #(nop) COPY file:a04afd1a50dad2d5…   3B
a83b62ac2ca6        About an hour ago   /bin/sh -c #(nop) COPY dir:f9f6ce2869336983a…   2.57kB
75ef1af2b83d        About an hour ago   /bin/sh -c npm ci                               416MB
aa88b6688a96        About an hour ago   /bin/sh -c #(nop)  ENV CI=1                     0B
ba417326cec3        About an hour ago   /bin/sh -c #(nop) COPY file:a86fcb846fef19bc…   79.5kB
78e0ee52d5a6        About an hour ago   /bin/sh -c #(nop) COPY file:91da48d89c17264e…   805B
3038f77e74b3        9 days ago          /bin/sh -c #(nop) WORKDIR /app                  0B
1613db8573fa        3 months ago        /bin/sh -c npm -v                               0B
acc9a9e3993d        3 months ago        /bin/sh -c node -v                              0B
6ca05a9fabe0        3 months ago        /bin/sh -c npm i -g npm@6                       37.2MB
fc56dfde691a        3 months ago        /bin/sh -c apt-get update &&   apt-get insta…   214MB
26cbfbc03e3f        3 months ago        /bin/sh -c #(nop)  CMD ["node"]                 0B
           3 months ago        /bin/sh -c set -ex   && for key in     6A010…   4.47MB
           3 months ago        /bin/sh -c #(nop)  ENV YARN_VERSION=1.6.0       0B
           3 months ago        /bin/sh -c ARCH= && dpkgArch="$(dpkg --print…   58.9MB
           3 months ago        /bin/sh -c #(nop)  ENV NODE_VERSION=10.0.0      0B
           4 months ago        /bin/sh -c set -ex   && for key in     94AE3…   129kB
           4 months ago        /bin/sh -c groupadd --gid 1000 node   && use…   335kB
           4 months ago        /bin/sh -c set -ex;  apt-get update;  apt-ge…   320MB
           4 months ago        /bin/sh -c apt-get update && apt-get install…   123MB
           4 months ago        /bin/sh -c set -ex;  if ! command -v gpg > /…   0B
           4 months ago        /bin/sh -c apt-get update && apt-get install…   44.6MB
           4 months ago        /bin/sh -c #(nop)  CMD ["bash"]                 0B
           4 months ago        /bin/sh -c #(nop) ADD file:bc844c4763367b5f0…   123MB
```

Linux dependencies necessary to run Cypress are large, and `node_modules` is huge too.

## Conclusion

If you plan to build a Docker image with Cypress end-to-end tests to serve in production, you **must** use the Docker multi-stage feature to avoid dragging dev dependencies into production.