Push and publish Docker images with GitHub Actions

AlessandroMinoccheri
4 min readMay 2, 2021

In many articles, I mentioned many times about using GitHub Actions because they are a good choice for a lot of reasons.
Nowadays I can admit that there is another choice that I have explored and used a lot these days.
What I mean is the functionality of pushing your docker image through your GitHub Actions during your CI process.

Usually, when I want to publish my docker images to DockerHub, I need to do it manually by the command line, like this:

Building the image

docker image build -t organization/project:0.1.0 .

Publishing to DockerHub


docker push organization/project:0.1.0

It’s not a lot of work, but every time you fix or add a new feature you need to remember to build a new image and publish it.
Usually, I try to avoid manual operations because human error is possible, and automating what is repetitive for me is a best practice everywhere.

So for this reason, in one open-source project Arkitect where I’m contributing nowadays, we have a Dockerfile that needs to be published every time there is a push on master, or a new release comes out.

I can build and publish the Docker image manually every time, but I prefer to avoid this and I have tried exploring GitHub Actions to automate this process.

Exploring GitHub’s actions to automate the process of publishing a docker image to DockerHub was interesting because I found a lot of other interesting GitHub actions and many projects that do the automation that I like.

First of all, I have inserted inside my GitHub project into Settings->Secrets, two important repository secrets:
- DOCKERHUB_USERNAME: this is your username on Dockerhub or the name of your organization
- DOCKERHUB_TOKEN: this is the token and you can get it going on DockerHub in Account Settings->Security. Here you can generate a new Access Token. You can take the value and put it on GitHub.

Next step, I have created a file for the workflow inside GitHub and named it docker-publish.yml

The file is something like this:

name: Arkitect
on:
push:
branches:
— '*'
tags:
— '*'
pull_request:
jobs:
publish_docker_images:
runs-on: ubuntu-latest
steps:
— name: Checkout
uses: actions/checkout@v2
— name: Docker meta
id: meta
uses: crazy-max/ghaction-docker-meta@v2
with:
images: phparkitect/phparkitect
tags: |
type=raw,value=latest,enable=${{ endsWith(GitHub.ref, 'master') }}
type=ref,event=tag
flavor: |
latest=false
— name: Login to DockerHub
if: GitHub.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
— name: Build and push
uses: docker/build-push-action@v2
with:
context: .
push: ${{ GitHub.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

But it’s not enough because this build will only start if the tests pass, so I have moved the content of this file inside my CI process to another workflow called: build.yml. This is the test suite.

So we need to create a new job that depends on the test job, thanks to the keyword “needs”, like this:

name: Arkitecton:
push:
branches:
— '*'
tags:
— '*'
pull_request:
jobs:
build:
runs-on: ubuntu-latest steps:
— uses: actions/checkout@v2
- name: Install PHP
run : //stuff to install PHP
- name: Test
run: ./bin/phpunit
publish_docker_images:
needs: build
runs-on: ubuntu-latest
if: GitHub.ref == 'refs/heads/master' || GitHub.event_name == 'release'
steps:
— name: Checkout
uses: actions/checkout@v2
— name: Docker meta
id: meta
uses: crazy-max/ghaction-docker-meta@v2
with:
images: phparkitect/phparkitect
tags: |
type=raw,value=latest,enable=${{ endsWith(GitHub.ref, ‘master’) }}
type=ref,event=tag
flavor: |
latest=false
— name: Login to DockerHub
if: GitHub.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
— name: Build and push
uses: docker/build-push-action@v2
with:
context: .
push: ${{ GitHub.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

In this example, as I said before, I have created a new job that depends on the job named “build”.
In this way, if the job named “build” fails, I don’t create a new docker image, because I want to create it only if the tests pass.

I have used these GitHub Actions:
crazy-max/ghaction-docker-meta@v2 : it extracts metadata (tags, labels) for Docker.
docker/build-push-action@v2: it builds and pushes Docker images with Buildx with the full support of the features provided by Moby BuildKit builder toolkit.

A condition that I have added to my docker push job is:

if: GitHub.ref == 'refs/heads/master' || GitHub.event_name == 'release'

Because I want to execute this job only:
- when there is a push on master
- when a new tag is created

This is necessary for me because I don’t want to create a new docker image every time a pull request is created.

When there is a push on master the workflow, create a new docker image with the tag “latest”.
When a new tag is released, the workflow creates a new docker image, with the tag equal to the tag of the project and the tag “latest” is recreated.
In this way, I am sure to publish the last version of the docker image every time with new features or bugs fixed.

In conclusion, using GitHub Actions saves me a lot of time and repetitive work every time I need to publish my docker image for my project.
There are a lot of other processes that can be automated that I would like to explore and try in my projects, so as soon as possible I will publish other articles about this argument.

--

--