Development notes

Thoughts, notes and ideas about development

Setup Github Actions for Hugo

This blog-post describes how to set up Github Actions for building and deploying Hugo using Docker container.

The described approach is a starting point for further improvements but it will automate the deployment of new blog posts to a remote server where your blog is hosted.

This blog post doesn’t explain every line of the configuration but describes the used approach and refer to the official Github Action documentation.

We’re going to achieve the following:

  1. push a new blog post to the master branch
  2. build a Docker image and push it to the Github Packages
  3. deploy the Docker image to the remote server

Create a Docker file

To render a static content of our blog I’ll use Docker image based on Alpine Linux.

Create a Docker file with the following content at the root of the project folder:

FROM nginx:alpine
COPY public /usr/share/nginx/html

Add .gitignore

To exclude generated static content to be pushed to the repository add the following .gitignore file:

/public
.idea/
data/
layouts/
resources/

Push all changes to the repository.

Create a personal access token

To pull and push Docker images from/to Github packages a personal access token should be configured. For more information read the Github help page

Add Github Actions to the repository

Github Actions may be added to the repository by one of the following ways:

  • create workflows file manually under .github/workflows
  • use an existing template from Github

I’ll describe how to use the Publish Docker Container action template.

Go to the blog Github project for which Github actions will be configured.

Select Actions tab

Find the Publish Docker Container template and click on Set up this workflow:

In case if the template is not displayed click on Workflows for Python, Maven, Docker and more... which looks like this:

Provide the workflow and file names.

A new workflow file will be created under .github/workflows/deploy-and-publish.yml

Find the IMAGE_NAME variable and update it to something more meaningful. In my case, it’s dev-pages. Right now the whole workflow file looks smth like this:

name: Deply_and_publish

on:
  push:
    # Publish `master` as Docker `latest` image.
    branches:
      - master

    # Publish `v1.2.3` tags as releases.
    tags:
      - v*

  # Run tests for any PRs.
  pull_request:

env:
  # TODO: Change variable to your image's name.
  IMAGE_NAME: dev-pages
  DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
  GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=no"
  DOCKER_LOGIN_COMMAND: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin

jobs:
  # Run tests.
  # See also https://docs.docker.com/docker-hub/builds/automated-testing/
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Run tests
        run: |
          if [ -f docker-compose.test.yml ]; then
            docker-compose --file docker-compose.test.yml build
            docker-compose --file docker-compose.test.yml run sut
          else
            docker build . --file Dockerfile
          fi
  # Push image to GitHub Packages.
  # See also https://docs.docker.com/docker-hub/builds/
  push:
    # Ensure test job passes before pushing image.
    needs: test

    runs-on: ubuntu-latest
    if: github.event_name == 'push'

    steps:
      - uses: actions/checkout@v2

      - name: Build image
        run: docker build . --file Dockerfile --tag image

      - name: Log into registry
        run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin

      - name: Push image
        run: |
          IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME
          
          # Change all uppercase to lowercase
          IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
          # Strip git ref prefix from version
          VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
          # Strip "v" prefix from tag name
          [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
          # Use Docker `latest` tag convention
          [ "$VERSION" == "master" ] && VERSION=latest
          echo IMAGE_ID=$IMAGE_ID
          echo VERSION=$VERSION
          docker tag image $IMAGE_ID:$VERSION
          docker push $IMAGE_ID:$VERSION

Click on the Start commit button.

Our build should fail because according to our Docker file we need to copy the public folder to the container folder /usr/share/nginx/html.

Go to the Action tab again. Now we should see our first failed workflow:

The error message will look like:

Fix failed workflow

To fix our failed workflow open .github/workflows/deploy-and-publish.yml and update test and push jobs

Update test job

Update test job so that Hugo is installed and static content generated before verification of docker-compose file. The test job should look like:

  # Run tests.
  # See also https://docs.docker.com/docker-hub/builds/automated-testing/
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true  # Fetch Hugo themes
          fetch-depth: 0    # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: 'latest'
          extended: true
          
      - name: Build
        run: hugo --minify

      - name: Run tests
        run: |
          if [ -f docker-compose.test.yml ]; then
            docker-compose --file docker-compose.test.yml build
            docker-compose --file docker-compose.test.yml run sut
          fi

Update push job

Update push job so that Hugo is installed and static content generated before building and pushing Docker image. Push job should look like:

push:
    # Ensure test job passes before pushing image.
    needs: test

    runs-on: ubuntu-latest
    if: github.event_name == 'push'

    steps:
      - uses: actions/checkout@v2

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: 'latest'
          extended: true
         
      - name: Build
        run: hugo --minify

      - name: Build image
        run: docker build . --file Dockerfile --tag image

      - name: Log into registry
        run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin

      - name: Push image
        run: !!! LEFT WITHOUT CHANGES. SEE ABOVE !!!

Here I used the following:

Now our workflow should be green on the Action tab.

A new generated Docker image may be found on the packages tab:

Open Your profile tab

Go to the Packages file

Deployment configuration

This section describes how to add and configure a deployment job to the Workflow. The described approach is not perfect but rather simple and works so far.

Options for improvements:

  • run Github runner in Docker container
  • configure ssh key at runtime

Add and configure self-hosted runners

To add a self-hosted runner please refer to the Github help page: Hosting your own runners

Open .github/workflows/deploy-and-publish.yml file and replace all runs-on: ubuntu-latest by runs-on: self-hosted

Generate ssh key

To deploy a new Docker image to the remote service we need to

  1. generate ssh key on the runner’s host machine
  2. add public key to the authorized_keys file on a remote server where the blog is running

To generate ssh key login to the runner’s machine and run (without a passphrase):

ssh-keygen

As a result, a public and a private keys will be generated at the ~/.ssh folder.

Copy all content from the public key ~/.ssh/id_rsa.pub

Add authorized_keys

  1. Login to the blog remote host
  2. Past the content of ~/.ssh/id_rsa.pub to the ~/.ssh/authorized_keys

Add deploy job

Now it’s time to add a deploy job.

Open .github/workflows/deploy-and-publish.yml file and add:

  deploy:
    needs: push
    runs-on: self-hosted
    steps:
      - name: Log into registry
        run: ssh bloguser@blog.host "$DOCKER_LOGIN_COMMAND"

      - name: connect via ssh and recreate docker image
        run: ssh bloguser@blog.host "cd ~/path_with_docker_compose_file && docker-compose pull && docker-compose up -d"

where

  • bloguser a user on the remote server where the blog is running and who will deploy a new Docker image
  • blog.host is the hostname or IP address of the remote blog server
  • path_to_docker_compose_file path to the folder where docker-compose.yml file is located.

Create docker-compose.yml file

Create docker-compose.yml file at the project root folder like:

version: '3'

services:
  hugo:
    image: docker.pkg.github.com/{GITHUB_USER}/{GITHUB_REPOSITORY_NAME}/{DOCKER_IAMGE_NAME}:latest
    restart: always
    container_name: your-blog-hugo-container-name
    hostname: your-blog-host-name
    ports:
      - 8384:80

where {GITHUB_USER}, {GITHUB_REPOSITORY_NAME} and {DOCKER_IAMGE_NAME} are self explained.

The full docker image path may be found on the Github Packages folder. See above.

Full version of .github/workflows/deploy-and-publish.yml

The complete version of .github/workflows/deploy-and-publish.yml looks like:

name: Deply_and_publish

on:
  push:
    # Publish `master` as Docker `latest` image.
    branches:
      - master

    # Publish `v1.2.3` tags as releases.
    tags:
      - v*

  # Run tests for any PRs.
  pull_request:

env:
  # TODO: Change variable to your image's name.
  IMAGE_NAME: dev-pages.info

jobs:
  # Run tests.
  # See also https://docs.docker.com/docker-hub/builds/automated-testing/
  test:
    runs-on: self-hosted

    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true  # Fetch Hugo themes
          fetch-depth: 0    # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: 'latest'
          extended: true
          
      - name: Build
        run: hugo --minify

      - name: Run tests
        run: |
          if [ -f docker-compose.test.yml ]; then
            docker-compose --file docker-compose.test.yml build
            docker-compose --file docker-compose.test.yml run sut
          fi

  # Push image to GitHub Packages.
  # See also https://docs.docker.com/docker-hub/builds/
  push:
    # Ensure test job passes before pushing image.
    needs: test

    runs-on: self-hosted
    if: github.event_name == 'push'

    steps:
      - uses: actions/checkout@v2

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: 'latest'
          extended: true
         
      - name: Build
        run: hugo --minify

      - name: Build image
        run: docker build . --file Dockerfile --tag image

      - name: Log into registry
        run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin

      - name: Push image
        run: |
          IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME
          
          # Change all uppercase to lowercase
          IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')

          # Strip git ref prefix from version
          VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')

          # Strip "v" prefix from tag name
          [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')

          # Use Docker `latest` tag convention
          [ "$VERSION" == "master" ] && VERSION=latest

          echo IMAGE_ID=$IMAGE_ID
          echo VERSION=$VERSION

          docker tag image $IMAGE_ID:$VERSION
          docker push $IMAGE_ID:$VERSION

  deploy:
    needs: push
    runs-on: self-hosted
    steps:
      - name: Log into registry
        run: ssh bloguser@blog.host "$DOCKER_LOGIN_COMMAND"

      - name: connect via ssh and recreate docker image
        run: ssh bloguser@blog.host "cd ~/path_to_docker_compose_file && docker-compose pull && docker-compose up -d"

comments powered by Disqus