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:

  1. Checkout the repo - nothing more nothing less than cloning repository to a worker that will execute the job.
  2. Build image - you guessed again, just a simple docker build.
  3. 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).
  4. Now, our job needs to log in to the DigitalOcean container registry to be able to push the image in the following steps.
  5. 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).
  6. 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:

  1. Create a Personal Access Token and save (or remember ;) ) it.
  2. 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.

GitHub Actions

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.