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.
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
Once you have the file, you can now run
docker build . --file Dockerfile -t ${Image_Name}
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}
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}"
Concrete example:
docker tag python-dev snakepy/python-dev:latest
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}"
After you have uploaded your image, you can pull it from another Dockerfile and require it:
FROM ${DOCKER_HUB_NAMESPACE}/${IMAGE_NAME}:${VERSION}
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.
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}"
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
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)
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:
docker / login-action
GitHub Action to login against a Docker registry
About
GitHub Action to login against a Docker registry.
Usage
Docker Hub
To authenticate against Docker Hub it's strongly recommended to create a personal access token as an alternative to your password.
GitHub Container Registry
To authenticate against the GitHub Container Registry, use the
GITHUB_TOKEN
for the best security and experience.docker / build-push-action
GitHub Action to build and push Docker images with Buildx
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.
Usage
In the examples below we are also using 3 other actions:
setup-buildx
action will create and boot a builder using by default thedocker-container
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 be able…docker / setup-buildx-action
GitHub Action to set up Docker Buildx
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 or the
build-push
action By default, thedocker-container
driver will be used to be able to build multi-platform images and export cache using a BuildKit container.nodes
outputUsage
Advanced usage
docker / setup-qemu-action
GitHub Action to configure QEMU support
About
GitHub Action to install QEMU static binaries.
Usage
Customizing
inputs
Following inputs can be used as
step.with
keysimage
tonistiigi/binfmt:latest
)platforms
arm64,riscv64,arm
; defaultall
)outputs
Following outputs are available
platforms
Contributing
Want to contribute? Awesome! You can find information about contributing to this project in the CONTRIBUTING.md
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!
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).
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:
And then something similar for the schedule event.
I thought of doing it in a similar way! :D But it didn't work yet. I will revisit this next week.
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
Hey this is really awesome! I will definitely implement this as well. Thank you for pointing this out!