Maintaining Container Images Within GitHub

Share on:

I have been using a collection of products to maintain docker images. GitHub now provides suite of products which encompass all I require. This post documents the process I followed to create a CI pipeline for a container running static web site built with Jekyll. The static web site content is created from a fork of chowdown. I maintain recipes markdown files using Visual Studio code and when I push changes to the ‘publish’ branch of my GitHub Repository this triggers a GitHub Action to create the container image and publish this to the GitHub Container Registry.

GitHub Package Process Overview

Fork and clone chowdown

I am more interested in the process so I started by forking chowdown. I look to automate where possible so used this opportunity to look at using the API to perform the fork.

## Create personal access token with permissions on repo
curl -u 'darrylcauldwell' https://api.github.com/repos/clarklab/chowdown/forks -d ''
## When promoted paste personal access token

With the fork in place I can look to make local clone and run the website under Jekyll.

git clone https://github.com/darrylcauldwell/chowdown.git
cd chowdown
## if running first time from form edit _config.yml and remove the value for key baseurl: otherwise it doesn't start properly
jekyll serve
## brower to http://127.0.0.1:4000

Rename Main Branch

The repository forked from looks like it is hosted from GitHub Pages and contains single gh-pages branch. I would like main branch to be called publish to reflect that this contains the source for current HEAD image.

git branch --list --all
* gh-pages
  remotes/origin/HEAD -> origin/gh-pages
  remotes/origin/gh-pages

git branch -m gh-pages publish
git push origin :gh-pages publish
git push origin –u publish
git branch --list --all
* publish
  remotes/origin/HEAD -> origin/publish
  remotes/origin/publish

Static File Web Site

Using ‘jekyll serve’ provide UX testing during development which is useful. The chowdown docker-compose.yml builds fro jekyll/jekyll image and runs jekyll serve to host the site. I would like to generate static file website where I can choose serving engine later. I can use ‘jekyll build’ which outputs the site to a folder (./_site by default). The chowdown repository lists folder /_site is in the .gitignore, this prevents the static file website being upstream. The GitHub Action will look to create docker image from upstream so I will remove /_site from the .gitignore.

jekyll build
git add .
git commit -m "added static file web site files"
git push

Docker Image

I’ll be looking to use NGINX to serve the site and would like the image to be as slim as possible so looking Alpine Linux. Copying the contents of /_site to the NGINX default path. In case of troubleshooting Pods being able to exec bash is useful so I look to install this.

# Dockerfile
FROM nginx:1.21.1-alpine
COPY _site /usr/share/nginx/html
# COPY nginx.conf /etc/nginx/nginx.conf
RUN apk add --no-cache bash

With the Dockerfile in place we can use this to build the container image locally. Once this is build we can run it and ensure that its working as we expect. When we’re happy its working can push this to the GitHub Container Registry.

docker build --tag ghcr.io/darrylcauldwell/chowdown:1.0 .
docker run -d -p 80:80 ghcr.io/darrylcauldwell/chowdown:1.0
# browse http://127.0.0.0:80 to ensure 
docker push ghcr.io/darrylcauldwell/chowdown:1.0

Event Driven Build

I would like a new version of the docker image be built every time a branch is merged to the main ‘publish’ branch. GitHub Actions offer me capability on occurence of an event (merge branch) trigger an action (build image). These particular container images will be ran on Raspberry Pi so need to use docker buildx to create for Arm platform

The GitHub Action is created and runs in context of the GitHub Repository, the GitHub Container Registry is under context of user or organisation. In order the GitHub Action can write images the package needs configuring so Actions have write access.

GitHub Package Access Writes

Once permissions are in place create a custom action using similar yaml to this:

# .github/workflows/create.yml
name: Create chowdown image

on:
  push:
    branches: [ publish ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v1
      
    - name: Log in to the Container registry
      uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
      with:
        registry: ghcr.io
        username: darrylcauldwell
        password: ${{ secrets.GITHUB_TOKEN }}
          
    - name: Build and push image
      id: docker_build
      uses: docker/build-push-action@v2
      with:
        platforms: linux/amd64,linux/arm64
        push: true
        tags: |
          ghcr.io/darrylcauldwell/chowdown:latest
          ghcr.io/darrylcauldwell/chowdown:1.0          

    - name: Image digest
      run: echo ${{ steps.docker_build.outputs.digest }}

The actions workflow definitio is within the repository so upon committing the changes the action should trigger and build the image.

Deploy Container To Kubernetes

We can look to test this by creating a Kubernetes deployment and expose this as a service.

kubectl create namespace chowdown
kubectl create deployment chowdown --image=ghcr.io/darrylcauldwell/chowdown:1.0 --namespace=chowdown
kubectl expose deployment chowdown --port=80 --type=ClusterIP --namespace=chowdown

This service will be exposed to public internet. I have a single static IP but expose multiple services to achieve this I use Kubernetes Ingress rules which allow routing based on URL and path. Save the following as file named chowdown-ingress.yaml.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: chowdown-ingress
  namespace: chowdown
  annotations:
    kubernetes.io/ingress.class: public
spec:
  rules:
  - host: www.theveggiechef.uk
    http:
      paths:
      - backend:
          service:
            name: chowdown
            port: 
              number: 80
        path: /
        pathType: Prefix

Once created apply the file to create the Ingress route and test using cURL.

kubectl apply -f chowdown-ingress.yaml

curl www.theveggiechef.uk