I’ve heard a lot of good things about Github Actions and decided to try it out myself.

My projects always include building Docker images and pushing them to a specific registry to deploy to staging or production. Before doing so I also run unit and integration tests.

I wanted to automate this process so that whenever I push to the master/main branch, Github Actions first run my tests, and if they succeed, it builds and pushes a Docker image to a privately hosted Docker registry. Which then starts up in my test/staging environment.

In this post I share what I came up with.

This example project is a monorepo that contains a server directory, which is a Nodejs API, and a frontend directory. All in the same Git repository.

This is the yaml file my research resulted in, for building the Nodejs API image. The workflow for the frontend code is very similiar so I wont get into that here.

Note that my Github Actions are running on a self hosted runner.


name: Server CI workflow

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]
jobs:
  unit-tests:
    runs-on: self-hosted
    defaults:
      run:
        working-directory: ./server
    strategy:
      matrix:
        node-version: [16.x]
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v2
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
        cache-dependency-path: server/package-lock.json
    - run: npm ci
    - run: npm run unit
  deploy:
    needs: unit-tests
    runs-on: self-hosted
    steps:
      -
        name: Checkout 
        uses: actions/checkout@v2
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
      - 
        name: Login to private registry
        uses: docker/login-action@v1
        with:
          registry: ${{ secrets.REGISTRY_URL }}
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}
      -
        name: Build and push
        uses: docker/build-push-action@v2
        with:
          context: ./server/
          file: ./server/Dockerfile
          builder: ${{ steps.buildx.outputs.name }}
          push: true
          tags: '${{ secrets.REGISTRY_URL }}/my-project/server:latest'
          cache-from: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/my-project/server:buildcache'
          cache-to: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/my-project/server:buildcache,mode=max'

Replace my-project with the name of your project.

The first job in the yaml-file runs the unit tests, if successful, the second job runs which logins to my private docker registry, builds my docker image, and pushes it to the registry.

Using secrets

The curly brackets represent Actions Secrets, which are injected when the Actions runs. I am using three secrets: registry url, registry username and registry password which are required to login to the docker registry. Read more about actions secrets here.

Please let me know if this post has helped you by commenting or reacting to the article.