Github Actions with Docker

2 Ways to Use Your Own Docker Image in Github Actions

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.

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

Sign up to my newsletter to enjoy more content:

0 0 votes
Article Rating
Subscribe
Notify of
guest

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Anony Mouse
Anony Mouse
1 year ago

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?