Published

- 6 min read

Streamlit Deployment Guide Part 2: GitHub Actions & ghcr.io

img of Streamlit Deployment Guide Part 2: GitHub Actions & ghcr.io

This showcases a GitHub Workflow walkthrough of building and publishing a Docker image to GitHub Container Registry. It continues a series detailing the process of deploying Streamlit app to Azure, broken down into the following parts:

  • Part 1: Containerizing a Streamlit app.
  • Part 2: GitHub Workflow for Building and Publishing to ghcr.io You are here 😊
  • Part 3: Azure Infrastructure via Terraform
  • Part 4: GitHub Workflow for Terraform Apply & Destroy

Do you want to deploy your Streamlit application on Azure right now? Use the template repository 🚀

TL;DR

See the completed GitHub Workflow.

Prerequisites

A basic understanding of GitHub Actions is required and the Workflow assumes you have the files from Part 1:

  • /app/Dockerfile
  • /app/app.py
  • /app/requirements.txt
  • /app/.streamlit/config.toml
  • gitversion.yml

If you have your own Dockerfile, ensure all the dependent files are available and are stored within /app folder for this Workflow to function correctly.

Create a GitHub Workflow

The Workflow file should be stored within the following folder: .github/workflows/. A concise name could be docker-image-build-and-push.yaml.

Completed Workflow

   name: Docker Image Build and Push

on:
  push:
    branches: ['main']
    paths:
      - 'app/**'
  workflow_dispatch:

# If this workflow is triggered again while a previous run is still in progress, GitHub will queue the new run until the previous one completes.
concurrency:
  group: build-and-push-image

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest

    env:
      REGISTRY: ghcr.io

    permissions:
      contents: write
      packages: write

    steps:
      # Checkout the repository
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # image name needs to be lowercase, some accounts have uppercase letters
      - name: Get owner/repo name and convert to lowercase
        id: get-image-name
        run: echo "image-name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT

      # Install GitVersion for versioning
      - name: Install GitVersion
        uses: gittools/actions/gitversion/setup@v1.1.1
        with:
          versionSpec: '5.x'

      # Execute GitVersion to get the version number
      - name: Use GitVersion
        id: gitversion
        uses: gittools/actions/gitversion/execute@v1.1.1

      # Set up Git user for tagging
      - uses: fregante/setup-git-user@v2

      # Create a new tag based on the version number
      - name: Create Tag
        run: git tag -a ${{ steps.gitversion.outputs.semVer }} -m "Auto-generated tag from GitHub Action."

      # Push the newly created tags to the repository
      - name: Push Tags
        run: git push origin --tags

      # Log in to the Container registry
      - name: Log in to the Container registry
        uses: docker/login-action@v3.2.0
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # Build and push the Docker image to the registry
      - name: Build and push Docker image
        uses: docker/build-push-action@v5.3.0
        with:
          context: ./app
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ steps.get-image-name.outputs.image-name }}:latest
            ${{ env.REGISTRY }}/${{ steps.get-image-name.outputs.image-name }}:${{ steps.gitversion.outputs.semVer }}

Workflow Walkthrough

Workflow Definition

   name: Docker Image Build and Push

The name of the workflow which is shown under the Actions tab. This allows you to set a readable name for your Workflow, if omitted the file name will be shown.

Triggers

   on:
  push:
    branches: ['main']
    paths:
      - 'app/**'
  workflow_dispatch:

The workflow triggers on pushes to the “main” branch and only if the changes are made in the ‘app’ directory. The ‘workflow_dispatch’ allows the workflow to be triggered manually.

Concurrency

   concurrency:
  - group: build-and-push-image

This ensures that if the workflow is triggered again while a previous run is in progress, the new run will queue until the previous one completes.

Jobs

   jobs:
  build-and-push-image:
    runs-on: ubuntu-latest

    env:
      REGISTRY: ghcr.io

There is a single job defined which covers the full behavior to build and push the image. ‘runs-on’ specifies it should run on the latest version of Ubuntu. ‘env’ allows you to set environment variables available to all tasks within the job. ‘REGISTRY’ represents the Docker registry and is set to ‘ghcr.io’

Permissions

   permissions:
  contents: write
  packages: write

Grants the necessary permissions for the job to interact with the repository. Contents permission is needed to checkout the repository and specifically write is needed to be able to tag the repository. Packages write is needed to publish the Docker image.

Steps

1. Checkout the Repository
   - name: Checkout repository
  uses: actions/checkout@v4
  with:
    fetch-depth: 0

Checks out the repository to the job runner. Allowing the runner to access the repository content.

2. Get Owner/Repo Name and Convert to Lowercase
   - name: Get owner/repo name and convert to lowercase
  id: get-image-name
  run: echo "image-name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT

Retrieves the repository name and converts it to lowercase for use as the Docker image name (lowercase name is required). You can easily use an env variable for your image name. This just allows it to be automated.

3. Install GitVersion for Versioning
   - name: Install GitVersion
  uses: gittools/actions/gitversion/setup@v1.1.1
  with:
    versionSpec: '5.x'

Installs GitVersion tool for semantic versioning. This tool is used to generate a version for our Docker image. See explanation of GitVersion

4. Execute GitVersion to Get the Version Number
   - name: Use GitVersion
  id: gitversion
  uses: gittools/actions/gitversion/execute@v1.1.1

Runs GitVersion execute to generate the semantic version number. It sets the version number as output and available as steps.gitversion.outputs.semVer

5. Set Up Git User for Tagging
   - uses: fregante/setup-git-user@v2

Sets up Git user for tagging purposes.

6. Create a New Tag Based on the Version Number
   - name: Create Tag
  run: git tag -a ${{ steps.gitversion.outputs.semVer }} -m "Auto-generated tag from GitHub Action."

Creates a new Git tag based on the version number generated by GitVersion.

7. Push the Newly Created Tags to the Repository
   - name: Push Tags
  run: git push origin --tags

Pushes the created tag to the GitHub repository.

8. Log in to the Container Registry
   - name: Log in to the Container registry
  uses: docker/login-action@v3.2.0
  with:
    registry: ${{ env.REGISTRY }}
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

Logs into the container registry using the GitHub actor and token.

9. Build and Push the Docker Image to the Registry
   - name: Build and push Docker image
  uses: docker/build-push-action@v5.3.0
  with:
    context: ./app
    push: true
    tags: |
      ${{ env.REGISTRY }}/${{ steps.get-image-name.outputs.image-name }}:latest
      ${{ env.REGISTRY }}/${{ steps.get-image-name.outputs.image-name }}:${{ steps.gitversion.outputs.semVer }}

Builds and pushes the Docker image to the specified registry with the appropriate tags. The context is set to the ‘app’ directory, and the image is tagged with both ‘latest’ and the semantic version.

What is GitVersion?

GitVersion is a tool that helps automate versioning of software projects based on Git commit history, providing a consistent and reliable way to generate version numbers for different branches and releases. Read the documentation

Generated Version Number Example

How the version is generated is based on how you set up your GitVersion configuration file. Assume we used the configuration file and start with version 1.0.0 along with using conventional commits. Example commit history and resulting versions:

  • fix: resolve login bug -> Generated Version: 1.0.1
  • feat: add user profile page -> Generated Version: 1.1.0

Example gitversion.yml Configuration

The provided GitVersion file contains configuration settings for the GitVersion tool, basically it defines the rules on how the version will be generated. See configuration documentation for in-depth explanations.

   assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
assembly-informational-format: '{InformationalVersion}'
mode: ContinuousDelivery
increment: Inherit
continuous-delivery-fallback-tag: ci
tag-prefix: '[vV]'
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
no-bump-message: '\+semver:\s?(none|skip)'
commit-message-incrementing: Enabled
merge-message-formats: {}
update-build-number: true