How to use the docker image to run Github Actions? How to use them to speed up the flows and stabilize tests? And when you should not use them?
This article assumes you have prior knowledge of github actions and what Docker is. If you need a beginner’s tutorial for github actions, click here.
Docker images are a great way to create consistency and avoid complex setup processes. For instance, in Vivid
we are using an image to run our visual regression tests. This reduces the flakiness that might arise from different OS versions, different browser versions and even missing fonts on various machines. It can also be used to raise a DB image with pre-made data to run tests on during your CI/CD process.
Here are two ways of using them in Github Actions.
Table of Contents
How to Run Your Workflow on Your Own Docker Image?
This one is easy. When you select a machine to run your workflow on, you can also state the image you would like to use. In Vivid
we have our own image that already has playwright
installed. This way, we can run the tests locally just like we run them in the CI and it doesn’t matter what machine the developer is using.
Here’s the workflow file:
name: 🎨 Test Visual Regression
on: workflow_call
jobs:
test:
runs-on: ubuntu-latest
container: drizzt99/vonage:1.2.0
steps:
- run: echo "Running 1.2.0"
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- run: apt-get install tar -y
- uses: actions/cache@v3
id: cache
with:
path: node_modules/
key: ${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
- run: npm run nx e2e components -- --task=local
- uses: actions/upload-artifact@v3
with:
name: visual-regression-artifact
path: test-results/
In the code snippet above, we see our whole visual regression flow. You can see the full file here.
You can see the on: workflow_call
that states this is a reusable workflow.
The docker “magic” happens in the following line:
container: drizzt99/vonage:1.2.0
This tells github actions
to run the test in a container of the image stated in this line. The drizzt99/vonage:1.2.0
image is published to the docker hub (you could use your own private hub) and pulled by github actions for you during the run (with various optimizations and caching to make this super fast.
From then on, all the job runs on this image.
How to Run Services in Containers During a Workflow?
Now let’s say you would like to run a service against a postgress DB. You could raise a DB and populate it with data on the run. You could also setup a mock DB docker image and set it as a service available for your workflow.
jobs:
container-job:
runs-on: ubuntu-latest
services:
communication-db:
image: drizzt99/communication-db
env:
POSTGRES_PASSWORD: {{ secrets.COMMUNICATION-DB-PASSWORD }}
ports:
- 5432:5432
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Run my service test
run: npm run test-communication-service
env:
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
The new property here is services
. In this property, we state various containers that will be available for the main process. In this case, we set up our DB:
The service’s label communication-db
is set and under it, we state the image we’d like to use (in this case, a pre-made image of a DB), pass environment variables (in this case, a password we saved as a repository secret), and a ports
property. The ports
property maps tcp port on the db container to the host.
Finally, we use the DB in our test. Note that we use localhost
because github actions maps the ports according to the ports
property.
Note that we can also use our own container to run the flow as we did in the former section by adding:
container: drizzt99/communication-service
to our job.
In this case, github actions maps all the ports automagically between services and the main container so we do not need to map our container. The configuration will look like this:
jobs:
container-job:
runs-on: ubuntu-latest
container: drizzt99/communication-service
services:
communication-db:
image: drizzt99/communication-db
env:
POSTGRES_PASSWORD: {{ secrets.COMMUNICATION-DB-PASSWORD }}
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Run my service test
run: npm run test-communication-service
env:
POSTGRES_HOST: communication-db
POSTGRES_PORT: 5432
When Not to Use Your Own Docker Images in Github Actions?
On September 16th I talked about optimizing github actions in code.talks 2022 in Hamburg. After the talk, people approached me for questions beyond the scope of the 15 minutes Q&A. One of them asked me about ARM architecture.
I did not have prior experience with it, and he explained that you cannot run a docker image created on an ARM machine on a different architecture without setting up an emulator. Apparently, the installation of the emulator and working with the image using the emulator made a process that takes minutes take almost an hour.
In this case, I offered the following solution – don’t run your ARM setup on github actions machines. You can trigger a call for an ARM architecture machine set on your cloud provider and wait for a response to continue the rest of the action’s flow.
One way to do it is to set up an HTTP request from your action. You could do it in many ways: from using curl
, through custom nodejs code, to using a custom action that does that like the http-request-action. Call the remote machine, wait for its response and use the data in the rest of the flow. Faster, cleaner and simpler.
The ARM architecture is one example, but I believe any example with a large resource overhead would fall into that category. The github actions machines are rather basic in computational power, so keep that in mind.
An example from our setup is our attempt to increase the number of visual tests parallel processes. Playwright has a built-in parallelism mechanism, so instead of running the tests serially, you could run them 3 or more at the same time. Locally on a strong modern laptop, it works great – but when we tried to run 10 parallel processes on a github actions machine, it took longer than running them serially. That’s because it consumed more resources than the basic github actions machine had.
Summary
In a previous article I shared 7 tips and tricks I wish I knew before starting with github actions. I went over using a docker image in a shallow manner. In this article, we saw to run our flow using a docker image and how to use images as services to be used in our flow.
Using docker is the industry standard for encapsulation and quick setup of complex configurations. You can use it to allow developers to run various environments on their end machine, improve CI/CD stability and deploy whole contained services to production.
There is more than one use case in which you should not use you container
or service
directly in github actions. That’s due to simple performance measurements – github actions are meant to automate simple processes. More complex ones should be delegated to external – more powerful – machines.
Thanks a lot to Yuval Bar Levi for the kind and thorough review of this article
Dockerhub is the default, should have mentioned how to use container images with authentication to a private registry, or even locally without pulling. Sometimes it’s justified for images to be heavy and when using self-hosted runners we may not want to attempt pulling them if they already exist locally.
For an article about using container images in GitHub actions, you spoke too little about them, just “add this one line and the magic happens”. What if it doesn’t happen?
Good points. Thanks!
Added to my to-do list!