Auto-deploy docker images to Docker Hub using GitHub actions
Tags: github, actions, devops, cicd
Published: 2024-01-20 23:02:22

---
title: Auto-deploy docker images to Docker Hub using GitHub actions
published: true
date: 2023-12-29 09:00:00 UTC
tags: github, actions, devops, cicd
canonical_url: https://lewismunyi.hashnode.dev/auto-deploy-docker-images-to-docker-hub-using-github-actions
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/owh7k3pjjlue1jjxrub6.jpg
---

{%- # TOC start (generated with https://github.com/derlin/bitdowntoc) -%}

## Table of Contents

- [Introduction](#introduction)
- [Project Setup](#project-setup)
- [Workflows](#workflows)
   * [Components of a workflow file](#components-of-a-workflow-file)
   * [Workflow: `Latest` tag](#workflow-raw-latest-endraw-tag)
   * [Workflow: `Release` tag](#workflow-raw-release-endraw-tag)
- [Testing](#testing)
- [Conclusion](#conclusion)

{%- # TOC end -%}

## Introduction

This tutorial assumes that you are already familiar with Docker and creating Dockerfiles. If not, see [this tutorial](https://dev.to/lewismunyi/fun-with-avatars-containerize-the-app-for-deployment-distribution-part-2-7hp) from the series [**Fun with Avatars**](https://dev.to/lewismunyi/series/26131) to get yourself up to speed. It also assumes you have a [Dockerfile](https://lewismunyi.hashnode.dev/avatars-as-a-service-a-creative-approach-to-generating-avatars-using-ai-part-2#heading-understanding-the-dockerfile) ready for deployment, and a [Docker Hub](https://hub.docker.com/) and [Github](https://github.com) account set up.

I will be using an existing project to walk you through the process. In [Part. 2](https://dev.to/lewismunyi/fun-with-avatars-containerize-the-app-for-deployment-distribution-part-2-7hp) 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:

```Bash
.
├── 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:

```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:

**name**
   * This is the name of the workflow. It will be displayed on the Github actions tab
```YAML
name: Publish latest prod docker image
```

**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](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows).
```YAML
# Configures this workflow to run every time a change is pushed to the branch called `master`.
on:
  push:
    branches: ['master']
```

**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.
```YAML
build-and-push-image:
```

**runs-on**
   * Specifies the OS environment in which the workflow will run eg ubuntu, Debian, etc.
```YAML
runs-on: ubuntu-latest
```

**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/`:

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

Add the following code to it:

```YAML
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:

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

Add the following code to it:

```YAML
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! 🎊

![screenshot showing successful workflow runs on github](https://cdn.hashnode.com/res/hashnode/image/upload/v1704289614233/f9574b6f-4d3b-423b-8c92-1bdc832e79b8.png)

![screenshot showing the images on dockerhub](https://cdn.hashnode.com/res/hashnode/image/upload/v1704289576334/4aa56695-9280-416e-8161-0e84bf5d4ee7.png)

## 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!**