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 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.
FROM cypress/base:10 RUN npm install # or even better: RUN npm ci RUN $(npm bin)/cypress run
With the rise of Docker it has became popular to build a Docker image and run it in production. For example, the Zeit.co Now service can deploy any Docker image on the cloud in seconds. But if this image includes testing tools, like Cypress, the size of the image goes up a lot, and it becomes a lot slower to deploy and start in production.
In this blog post I will show you how to use Docker multi-stage builds 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.
I have described building several Docker images from a single Dockerfile in the blog post - Making small Docker image. 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.
# 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.
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. What we really want during the
docker build . execution:
- Only re-install NPM dependencies if the
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:
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.jsonit will re-install Cypress and will execute the E2E tests
- If you only change spec files inside the
npm installcommand will be skipped because the
package.jsonfile has not changed, and the Docker
buildcommand is smart to pull a cached layer image. Only
RUN npm testwill be executed
- If nothing changes, no commands will be run - and
docker build .will finish quickly.
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 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.
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
docker build . --build-arg BUST=$(date +%s)
Imperfect, but works.
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 image to statically serve the
public folder. Compared to
cypress/base:10 + NPM dev dependencies (which includes unzipped Cypress) it is tiny.
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
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
$ 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 [email protected] 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.
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.