Push and publish Docker images with GitHub Actions
AlessandroMinoccheri
Posted on May 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: Arkitect
on:
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.
Posted on May 2, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.