Automating software delivery became a standard in the modern development process. There are dozens of tools available for enterprise as well as open-source projects. Today we will take a closer look at the GitHub Actions - a toolset that can be used with both, private and public projects and connect it with a private container registry at DigitalOcean.
In this short tutorial you will learn:
- Basics of Docker images sharing,
- How to automate building and publishing Docker images using GitHub Action,
- How to connect to a private container registry at DigitalOcean.
To complete this tutorial, you will need:
- Docker installed,
- DigitalOcean account with container registry created (in
this article we will use registry named
my-sample-registry
), - GitHub account.
Ingredients
GitHub Actions
Actions is a very simple automation tool that enables your GitHub repository (no matter private or public) to start using CI/CD to build, test, and deploy your code with zero costs. This is a fantastic feature of GitHub that enables open-source projects to automate repetitive tasks and introduce quality control.
Docker Registry
Docker has been with us for over 10 years now. Probably it is not said enough that it had a huge impact on the popularization of containers. Many of the modern software applications are based on utilising the container approach thanks to its low entry-level and simplicity it brings to running services.
To run a containerized application, all you need is an image. You may compare Docker image to a CD - once burned, it never changes. Every time you run it, no matter where: developer’s instance, staging, or production, the effect is guaranteed to be the same (which is one of the greatest advantages of the container approach).
However, to move a container-based software outside the developer's instance, you need a registry. And the thing that connects docker image and registry are called repositories. Wrapping it up, container registry is a service that enables sharing and distributing repositories of images.
The last concept to mention are tags. It is a common convention that tags are used for semantic versioning of images,
but they were designed to meet a more general purpose. Think of tags as an alias for a specific variant of an image
(e.g. 0.0.1
but also windows-based
/linux-based
).
Recipe
To obtain a Docker image, you need to build it first. Let’s start with a very basic image that will use Nginx to serve a static HTML page.
But first - create a new GitHub repository following the official guide (no matter public or private - both can use GitHub Actions). For simplicity, initialize it with a README file. Clone the repository and open it as your project root directory.
Image
Create a file named Dockerfile
with the following content:
FROM nginx:latest
COPY ./html/hello.html /usr/share/nginx/html/hello.html
In the first line, you can see the FROM
command, which defines a parent image. Docker image consists of layers, and
every command in Dockerfile (image’s definition) creates a new layer. The final size of an image is the sum of all
layers (don’t worry, what is great about Docker images, is that they can share common layers including parent images).
In the second line, you copy a static hello.html
file to the Nginx html root directory. Let’s create hello.html
in
the html
directory, next to the Dockerfile:
<!DOCTYPE html>
<html>
<head>
<title>Hello World!</title>
</head>
<body>
<p>This is an example of a simple HTML page served from the Nginx container.</p>
</body>
</html>
Now you should be able to run docker build -t sample/my-page .
. That will produce a sample/my-page
image.
To run a container, execute docker run -it --rm -p 9999:80 --name my-website sample/my-page
and open
http://localhost:9999/hello.html.
That’s it, you have a working Nginx instance serving an HTML page.
Workflow
The next missing thing is Action workflow. Let’s create one that will build the image from the previous step and push it to the container registry on demand.
In the root of your repository create .github/workflows/webapp-publish-on-release.yml
file.
Next, use the following configuration to fill the webapp-publish-on-relaese.yml
file’s content:
name: Build and publish manually
on:
workflow_dispatch:
inputs:
version:
description: 'Image version'
required: true
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo (1)
uses: actions/checkout@v2
- name: Build image (2)
run: docker build -t sample/my-page .
- name: Install doctl (3)
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Log in to DO Container Registry (4)
run: doctl registry login --expiry-seconds 600
- name: Tag image (5)
run:
docker tag sample/my-page \
registry.digitalocean.com/my-sample-registry/my-sample-page:${{github.event.inputs.version }}
- name: Push image to DO Container Registry (6)
run: docker push registry.digitalocean.com/my-sample-registry/my-sample-page:${{ github.event.inputs.version }}
Besides the obvious properties as name
, there is the trigger (on
) and a couple of steps that form the
build_and_push
job (if you need more details on the structure, please refer to the
components of GitHub Actions).
There are many triggers to choose from. For this example, we will use the manual trigger (workflow_dispatch
) with an
additional input parameter: version
.
Now, let’s go through the job step by step:
- Checkout the repo - nothing more nothing less than cloning repository to a worker that will execute the job.
- Build image - you guessed again, just a simple
docker build
. - Install doctl - things are finally getting interesting. In order to push an image to the DigitalOcean registry, you
will need a CLI for the DigitalOcean API - and there is one called
doctl
. The same is true for the workflow you are building here. Fortunately, we have a nice action for that -digitalocean/action-doctl@v2
. Notice that the token used in this step comes from the GitHub Secrets (we will talk about it in a moment). - Now, our job needs to log in to the DigitalOcean container registry to be able to push the image in the following steps.
- You need to tag the image with the proper registry, repository, and image name. In this example, we use the input parameter to tag a freshly built image with a version number (the command is split into 2 lines for better readability).
- Finally, the image is pushed to the repository.
Don’t forget to push changes to your repository.
Your repository should finally have a similar structure:
.
├── .github
│ └── workflows
│ └── webapp-publish-on-relaese.yml
├── Dockerfile
├── README.md
└── html
└── hello.html
Integration
The last thing left is creating GitHub Secret with DigitalOcean Access Token. Follow these steps in order to generate and add a token to GitHub Secrets:
- Create a Personal Access Token and save (or remember ;) ) it.
- Create an encrypted secret for your GitHub repository:
the name would be
DIGITALOCEAN_ACCESS_TOKEN
(the same defined in the workflow) and the value would be of course the token value you generated in the previous step.
That’s all! Now, you can run the workflow. Navigate to the Actions
tab in the GitHub repository view, choose the
Build and publish manually
workflow from the tree on the left and execute Run workflow
. Set the version of your
choice.
Important notice. To run Actions manually, at the moment they must be merged into the main repository branch.
Congratulations! You have just built a nice Continuous Integration workflow, that enables you to automate publishing Docker images to a private container registry.
Summary
Through this short article, you learned what is a Docker image, repository, and registry as well as how to use them to share your containerized application. Additionally, you set up a private container registry using DigitalOcean and pushed a Docker image using GitHub Actions.
You can find a full code example on my GitHub repository: malaskowski/push-docker-to-digitalocean-with-gh-action.