Auto-deploy docker images to Docker Hub using GitHub actions

Introduction

This tutorial assumes that you are already familiar with Docker and creating Dockerfiles. If not, see this tutorial from the series Fun with Avatars to get yourself up to speed. It also assumes you have a Dockerfile ready for deployment, and a Docker Hub and Github account set up.

I will be using an existing project to walk you through the process. In Part. 2 of the series, we containerized the app created but we encountered a few problems:

  1. While the image would build successfully, any changes made to the app's files would not reflect in containers until the image was rebuilt and the containers recreated.

    • We fixed this problem by having separate dockerfiles for development and production, and by mounting our host drive to the docker VM using docker volumes.
  2. We have to publish any new tags we build to Docker Hub manually. This works fine for a few changes but would quickly disrupt our workflow on active development.

    • We will employ Github actions to alleviate this by rebuilding and redeploying the prod image to Docker Hub for every update to our main branch on Github.

Project Setup

The service we are auto-deploying is a FastAPI service for creating avatars. This is the current project structure:

.
├── Dockerfile
├── LICENSE
├── README.md
├── README.rst
├── avatars_as_a_service
│   ├── __init__.py
│   ├── database
│   │   ├── aaas.sqlite.db
│   │   └── connection.py
│   ├── enums
│   │   └── AvatarFeatures.py
│   ├── models.py
│   ├── schemas.py
│   └── search.py
├── development.Dockerfile
├── main.py
├── poetry.lock
├── pyproject.toml
└── tests
    ├── __init__.py
    └── test_avatars_as_a_service.py

The Dockerfile:

# Python base image
FROM python:3.10

LABEL maintainer="Lewis Munyi"

LABEL environment="production"

# https://stackoverflow.com/questions/59812009/what-is-the-use-of-pythonunbuffered-in-docker-file
ENV PYTHONUNBUFFERED=1
ENV POETRY_VERSION=1.7.1

# Set the working directory inside the container
WORKDIR /api

# Copy the application code to the working directory
COPY . .

# Uncomment the following lines if you are using Pip not poetry
# Install the Python dependencies
#RUN pip install -r requirements.txt

# Install poetry & packages
RUN curl -sSL https://install.python-poetry.org | python3 -

# Update path for poetry
ENV PATH="${PATH}:/root/.local/bin"

# https://python-poetry.org/docs/configuration/#virtualenvscreate
RUN poetry config virtualenvs.create false
RUN poetry install --no-root --no-interaction

# Create app database
RUN mkdir -p avatars_as_a_service/database
RUN touch avatars_as_a_service/database/aaas.sqlite.db

# Expose the port on which the application will run
EXPOSE 8000

# Run the FastAPI application using uvicorn server
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Workflows

GitHub actions are executed by workflows specified in YAML files. Below are the main components of the file:

Components of a workflow file

The YAML file contains the specifications for running our workflow and building our image. These are some of the components we will be using along with their purpose:

  1. name

    • This is the name of the workflow. It will be displayed on the Github actions tab

        name: Publish latest prod docker image
      
  2. on

    • Specifies the events that trigger the workflow such as a push to a specific branch or a new release. See a list of supported Github events here.

        # Configures this workflow to run every time a change is pushed to the branch called `master`.
        on:
          push:
            branches: ['master']
      
  3. jobs

    • Specifies the job that's running as part of the workflow. You can have many jobs running as part of a workflow such as compiling an app package, deploying it to a registry and also deploying containers to different registries in one workflow.

        build-and-push-image:
      
  4. runs-on

    • Specifies the OS environment in which the workflow will run eg ubuntu, Debian, etc.

        runs-on: ubuntu-latest
      
  5. steps

    • Contains a sequence of tasks to be executed as part of the job. The steps will contain a name and the GitHub action it uses to perform an action

Workflow: Latest tag

Create a file called publish-latest-to-docker-hub.yml in the directory .github/workflows/:

mkdir -p .github/workflows && touch .github/workflows/publish-latest-to-docker-hub.yml

Add the following code to it:

name: Publish latest prod image

# Configures this workflow to run every time a change is pushed to the branch called `master`.
on:
  push:
    branches: ['master']

jobs:
  build-and-push-image:
    name: Push latest docker image to Docker Hub
    runs-on: ubuntu-latest
    steps:
      - name: Check out the repo
        uses: actions/checkout@v4

      - name: Log in to Docker Hub
        uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
        with:
          # Set these on your Github Settings page under repository secrets
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
        with:
          images: my-docker-hub-namespace/my-docker-hub-repository

      - name: Build and push Docker image
        uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: my-docker-hub-namespace/my-docker-hub-repository:latest
          labels: ${{ steps.meta.outputs.labels }}

Workflow: Release tag

Lastly, create a workflow file that will publish specific releases once published:

touch .github/workflows/publish-release-to-docker-hub.yml

Add the following code to it:

name: Publish release to Docker Hub

# Configures this workflow to run every time a release is published.
on:
  release:
    types: [published]

jobs:
  build-and-push-image:
    name: Push docker image to Docker Hub
    runs-on: ubuntu-latest
    steps:
      - name: Check out the repo
        uses: actions/checkout@v4

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_PASSWORD }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
        with:
          images: ${{ secrets.DOCKER_HUB_USERNAME }}/avatars-as-a-service

      - name: Set Docker tag
        id: set_tag
        run: echo ::set-output name=tag::$(echo $GITHUB_REF | awk 'BEGIN{FS="/"} {print $3}')

      - name: Build Docker Image
        run: docker build -t ${{ steps.set_tag.outputs.tag }} .

      - name: Tag Docker Image
        run: docker tag ${{ steps.set_tag.outputs.tag }} ${{ secrets.DOCKER_HUB_USERNAME }}/avatars-as-a-service:${{ steps.set_tag.outputs.tag }}

      - name: Push Docker Image
        run: docker push ${{ secrets.DOCKER_HUB_USERNAME }}/avatars-as-a-service:${{ steps.set_tag.outputs.tag }}

Testing

Workflows now publish new versions to Docker Hub automatically when run. Yay! 🎊

Conclusion

That brings us to the end of this article. In the next article, we will learn how to pull and run the service on a web application.

Cheers!