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:
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
andproduction
, and by mounting our host drive to the docker VM using docker volumes.
- We fixed this problem by having separate dockerfiles for
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:
name
This is the name of the workflow. It will be displayed on the Github actions tab
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.
# 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.
build-and-push-image:
runs-on
Specifies the OS environment in which the workflow will run eg ubuntu, Debian, etc.
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 ituses
to perform an action
- Contains a sequence of tasks to be executed as part of the job. The steps will contain a
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!