DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Fabio
Fabio

Posted on • Updated on

Publish your own Docker Images with GitHub Actions

In this post, I will show how easily you can set up your own docker images and then utilize GitHub Actions to deploy them to a Docker registry. This will save you set up time and make your environments more stable.

Prerequisites

  • Git installed and GitHub Account
  • Docker installed (for testing)

Get Started with Docker

Docker is a virtualization tool which allows you to require an image which comes with pre-installed configurations and software. So you can pick an operating system of your liking (NO IOS that's illegal) and then get started with installing what you need. These definitions will be written into a Dockerfile. If you don't have docker installed, head to their docs it is really simple to install it

Write your first image

Let's start with a very simple Python image. We will pull the latest distribution of Python and set the PYTHONUNBUFFERED=1 to ensure that all python output is sent to the terminal for better debugging. Paste the below lines into a Dockerfile (Yes the file has no file extension).

FROM python:latest
ENV PYTHONUNBUFFERED=1
Enter fullscreen mode Exit fullscreen mode

Once you have the file, you can now run

docker build . --file Dockerfile -t ${Image_Name}
Enter fullscreen mode Exit fullscreen mode

The --file can be used in case your Dockerfile has not the default name or is in another path. The -t flag is used to give the image a name. After the image build successfully on your machine, you can go ahead and try to publish it to hub.docker.com.

Next, we want to tag our image, therefore please create an account at docker hub. We will need the namespace. Before proceeding, please log in locally with docker login.

docker login --username ${USERNAME}
Enter fullscreen mode Exit fullscreen mode

This will log you in and store an encrypted version of your credentials for later use locally.

To version our image we use tags, so if one image is bad due to any reason, you can always pull an earlier image. The following command will tag the image. The DOCKER_HUB_NAMESPACE is by default your username in docker hub. So for me, it would be snakepy.

docker tag "${IMAGE_NAME}" "${DOCKER_HUB_NAMESPACE}/${IMAGE_NAME}:${VERSION}"
Enter fullscreen mode Exit fullscreen mode

Concrete example:

docker tag python-dev snakepy/python-dev:latest
Enter fullscreen mode Exit fullscreen mode

After this, all what is left is to upload the image to docker hub, so you can use it later, therefore you need to run:

docker push "${DOCKER_HUB_NAMESPACE}/${IMAGE_NAME}:${VERSION}"
Enter fullscreen mode Exit fullscreen mode

After you have uploaded your image, you can pull it from another Dockerfile and require it:

FROM ${DOCKER_HUB_NAMESPACE}/${IMAGE_NAME}:${VERSION}
Enter fullscreen mode Exit fullscreen mode

WorkFlow for automation

I have created a repository where I upload my images to. The workflow is set up in a way that if I push to that repository it will automatically build all images and push them to docker hub with a new version. I also created scheduled tasks, which runs periodically to release the latest version of the image.

This is where the fun begins and once you have your automation set up you can refine it, and it will grow overtime. You can have a look at my current set up.

To automate the creation of docker images, you need to do the following steps:

  • create a repository for your images
  • set the secrets in the repository
  • create workflow file
  • profit πŸ’°

First create the GitHub repository and then find the secrets tab, there you can add new secrets. You will need to add DOCKER_HUB_NAMESPACE, DOCKER_HUB_PASSWORD andDOCKER_HUB_USER. You can either set them as repository or workflow env. I found it easier to simply set them as repository variable.

Screenshot of github secrets

After you have added the secrets, we can proceed to the GitHub workflow file. The file(s) need to be inside .github/workflows. In there I created two files, one for scheduled tasks and one if I push. This will help us to deal easier with the versioning of the images.

name: publish-to-docker-hub
on: [push]

env:
Β  DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
Β  DOCKER_HUB_USER: ${{ secrets.DOCKER_HUB_USER }}
Β  DOCKER_HUB_NAMESPACE: ${{ secrets.DOCKER_HUB_NAMESPACE }}
Β  VERSION: ${{ github.sha }}

jobs:
  publish_python:
    runs-on: ubuntu-latest
    env:
      IMAGE_NAME: 'python-dev'
      LANGUAGE: 'python'
      LANGUAGE_VERSION: 3.10
    steps:
      - uses: actions/checkout@v2
      - run: echo ${DOCKER_HUB_PASSWORD} | docker login --username "${DOCKER_HUB_USER}" --password-stdin
      - run: docker build . --file ${IMAGE_NAME}/Dockerfile -t ${IMAGE_NAME}
      - run: docker tag "${IMAGE_NAME}" "${DOCKER_HUB_NAMESPACE}/${IMAGE_NAME}:${LANGUAGE}${LANGUAGE_VERSION}-${VERSION}"
      - run: docker push "${DOCKER_HUB_NAMESPACE}/${IMAGE_NAME}:${LANGUAGE}${LANGUAGE_VERSION}-${VERSION}"
Enter fullscreen mode Exit fullscreen mode

First, we define the name of the workflow publish-to-docker-hub and then the action trigger. Next, we define the variables which will be pulled from the GitHub secret store. Then we define our jobs. Under Jobs, you can have multiple jobs (checkout my YAML file). We are essentially doing what I showed before. We are logging into docker hub, building the image, tagging the image and uploading the image.

Please note my repository structure! Every Dockerfile is placed like this ${IMAGE_NAME}/Dockerfile. This way I can write small documentations with it or even have config files which I can copy in during build time.

Now you should see that the action is created for every push you do. I also like an action once a month to release the latest version of the image, just to keep them fresh.

So I created a second workflow file which is essentially the same and the only difference is the trigger and the VERSION variable.

on:
Β  schedule:
Β  Β  - cron: '0 0 1 * *'


env:
Β  DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
Β  DOCKER_HUB_USER: ${{ secrets.DOCKER_HUB_USER }}
Β  DOCKER_HUB_NAMESPACE: ${{ secrets.DOCKER_HUB_NAMESPACE }}
Β  VERSION: latest
Enter fullscreen mode Exit fullscreen mode

LET ME KNOW πŸš€

  • Do you need help, with anything written above?
  • What will be your first Image? πŸ˜„
  • Do you think I can improve - then let me know
  • Did you like the article? πŸ”₯

Check my laravel-dev image out! πŸ”₯

Top comments (7)

Collapse
cicirello profile image
Vincent A. Cicirello

You have a nice straightforward approach. I like it.

The docker organization on GitHub has several useful actions you might look at if you need to do something more complex, such as multiplatform images or pushing to multiple registries. For example, here is a link to a workflow in one of my repos where on releases I build a multiplatform image and push to both Docker Hub as well as the GitHub Container Registry. The docker/login-action is especially useful for that in combination with the docker/build-push-action. If you are pushing to N registries, you need N applications of the login-action, but just one build-push-action.

Links to the specific docker actions I'm using are below:

GitHub logo docker / login-action

GitHub Action to login against a Docker registry

GitHub release GitHub marketplace CI workflow Test workflow Codecov

About

GitHub Action to login against a Docker registry.

Screenshot


Usage

Docker Hub

To authenticate against Docker Hub it's strongly recommended to create a personal access token as an alternative to your password.

name: ci

on:
  push:
    branches: main

jobs:
  login:
    runs-on: ubuntu-latest
    steps:
      -
        name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

GitHub Container Registry

To authenticate against the GitHub Container Registry, use the GITHUB_TOKEN for the best security and experience.

name: ci
on:
  push:
    branches: main

jobs:
  login
…
Enter fullscreen mode Exit fullscreen mode

GitHub logo docker / build-push-action

GitHub Action to build and push Docker images with Buildx

GitHub release GitHub marketplace CI workflow Test workflow Codecov

About

GitHub Action to build and push Docker images with Buildx with full support of the features provided by Moby BuildKit builder toolkit. This includes multi-platform build, secrets, remote cache, etc. and different builder deployment/namespacing options.

Screenshot


Usage

In the examples below we are also using 3 other actions:

  • setup-buildx action will create and boot a builder using by default the docker-container builder driver This is not required but recommended using it to be able to build multi-platform images, export cache, etc.
  • setup-qemu action can be useful if you want to add emulation support with QEMU to…

GitHub logo docker / setup-buildx-action

GitHub Action to set up Docker Buildx

GitHub release GitHub marketplace CI workflow Test workflow Codecov

About

GitHub Action to set up Docker Buildx.

This action will create and boot a builder that can be used in the following steps of your workflow if you're using buildx. By default, the docker-container builder driver will be used to be able to build multi-platform images and export cache thanks to the BuildKit container.

Screenshot


Usage

Quick start

name: ci
on:
  push:

jobs:
  buildx:
    runs-on: ubuntu-latest
    steps:
      -
        name: Checkout
        uses: actions/checkout@v3
      -
        name: Set up Docker Buildx
        id: buildx
        uses: docker/setup-buildx-action@v2
      -
        name: Inspect builder
        run: |
          echo "Name:      ${{ steps.buildx.outputs.name }}"
          echo "Endpoint:  ${{ steps.buildx.outputs.endpoint }}"
          echo "Status:    ${{ steps.buildx.outputs.status }}"
…
Enter fullscreen mode Exit fullscreen mode

GitHub logo docker / setup-qemu-action

GitHub Action to configure Qemu support

GitHub release GitHub marketplace CI workflow

About

GitHub Action to install QEMU static binaries.

Screenshot


Usage

name: ci

on:
  push:

jobs:
  qemu:
    runs-on: ubuntu-latest
    steps:
      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@v2
Enter fullscreen mode Exit fullscreen mode

Customizing

inputs

Following inputs can be used as step.with keys

Name Type Description
image String QEMU static binaries Docker image (default tonistiigi/binfmt:latest)
platforms String Platforms to install (e.g. arm64,riscv64,arm ; default all)

outputs

Following outputs are available

Name Type Description
platforms String Available platforms (comma separated)

Keep up-to-date with GitHub Dependabot

Since Dependabot has native GitHub Actions support, to enable it on your GitHub repo all you need to do is add the .github/dependabot.yml file:

version: 2
updates:
  # Maintain dependencies for GitHub Actions
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "daily"
Enter fullscreen mode Exit fullscreen mode



Collapse
snakepy profile image
Fabio Author

Hey thank you for your feedback!

I knew I can do better than what I have! I very recently got into githubworkflows, I am not used to using premade jobs. In my company we use gitlab for our workflow and there you usually code it yourself. But I definitly gonna check the repositories out!

I already had a look into your workflow file. I must admit I find my solution more readable. I miss YAML Anchors so I can make it even more readable.

But I want to improve it and condense it to one file and yes currently I am just deplying to one registry, but this might change!

Collapse
cicirello profile image
Vincent A. Cicirello

Your workflow is very readable. The step I named prepare is almost certainly more complicated than it needs to be. It is getting the release tag from the github release that triggered the workflow, and generating a list of tags for the image. From a github release tag of let's say v3.2.1, I'm dropping the v and tagging the docker image with: latest, 3.2.1, 3.2, and 3 (the step that does all of that looks very messy).

Collapse
cicirello profile image
Vincent A. Cicirello

If you are hoping to condense the 2 workflows into one, you can probably use a conditional step that checks which event triggered that run. For example:

Β Β Β Β Β Β - if:Β ${{Β github.event_nameΒ ==Β 'push'Β }}
        run: # thing you want to do on a push
Enter fullscreen mode Exit fullscreen mode

And then something similar for the schedule event.

Thread Thread
snakepy profile image
Fabio Author

I thought of doing it in a similar way! :D But it didn't work yet. I will revisit this next week thought.

Collapse
yani profile image
Youcefi Mohammed Yassine

Hi i like your post and i want to give a note, so about writing a small documentation for your release and write the tag manually, setup your fully automated version management, like github.com/semantic-release/semant...
And use commitizen for get a nice changelog

Collapse
snakepy profile image
Fabio Author

Hey this is really awesome! I will definitely implement this as well. Thank you for pointing this out!

🌚 Browsing with dark mode makes you a better developer.

It's a scientific fact.