Since the source code of my website is kept on Github, using Github Actions to automate building and deploying my website seems to be a natural choice. Plus, it’s relatively simple to setup.

Step 1: Create the Workflow YAML

I’m using Hugo and Github provides a pretty good template for Hugo so it’s a good start. The only problem is that the template helps you deploy the website to Github Pages, but I need to deploy to my own server.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
name: Deploy Hugo site to mywebsite.com

on:
  # Runs on pushes targeting the default branch
  push:
    branches: ["master"]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
  group: "pages"
  cancel-in-progress: false

# Default to bash
defaults:
  run:
    shell: bash

jobs:
  # Build the website and deploy the generated static site to the server
  build-and-deploy:
    runs-on: ubuntu-latest
    env:
      HUGO_VERSION: 0.112.5
    steps:
      - name: Install Hugo CLI
        run: |
          wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
          && sudo dpkg -i ${{ runner.temp }}/hugo.deb          
      - name: Install Dart Sass
        run: sudo snap install dart-sass
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: recursive
      - name: Get short SHA
        id: short_sha
        run: echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
      - name: Build with Hugo
        env:
          # For maximum backward compatibility with Hugo modules
          HUGO_ENVIRONMENT: production
          HUGO_ENV: production
        run: hugo --minify
      - name: Deploy to mywebsite.com
        env:
          SSH_PRIVATE_KEY: ${{ secrets.MYWEBSITE_COM_KEY }}
        run: |
          mkdir ~/.ssh && echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H mywebsite.com >> ~/.ssh/known_hosts          
        
          DEPLOY_DIR=anakinfoxe.com_${{ steps.short_sha.outputs.SHA_SHORT }}_$(date +%Y%m%d%H%M%S)
          ssh deployer@mywebsite.com "mkdir -p /var/www/$DEPLOY_DIR"
          rsync -avz --delete public/ deployer@mywebsite.com:/var/www/$DEPLOY_DIR
          
          ssh deployer@mywebsite.com "ln -snf /var/www/$DEPLOY_DIR /var/www/mywebsite.com"
          ssh deployer@mywebsite.com "find /var/www/ -maxdepth 1 -type d -name 'mywebsite.com_*' -mtime +180 -exec rm -rf {} \;"

Here is the main steps of the workflow:

  • branches: ["master"] defines the condition to trigger the workflow: a push event onto the master branch.
  • In jobs defines only one job: build-and-deploy which will generate static website and deploy it to the server. Most configs and steps were provided in the template except:
    • Get short SHA will be used to name the folder uploaded to the server
    • Deploy to mywebsite.com
      • Use the SSH private key to login to the server
      • Use rsync to recursively sync generated static website to the server
      • Create symbolic link to the website root directory
      • Remove previous uploads that are more than 180 days old

Why use one job to build and deploy

Instead of using a build job and a deployment job? Because that will require me to upload the generated static website to somewhere (for example, Github Pages) once the build is done, and then download it when the deployment job starts. I feel it’s not necessary for this simple project.

Step 2: Prepare User

An user “deployer” will be used to login to the server and does all the deployment work. I don’t want to use any existing users on the server so I can properly manage its permission and capability.

  1. Create the “deployer” user as I used in the workflow yaml, and create SSH folder
1
2
3
4
sudo adduser --disabled-password --gecos "" deployer

sudo mkdir /home/deployer/.ssh 
sudo chown deployer:deployer /home/deployer/.ssh
  1. Generate SSH keys for the user on the local computer
1
ssh-keygen -t ed25519 -C "deployer@mywebsite.com" -f ~/.ssh/github_actions_deployer

Don’t provide any passphrase. Otherwise it needs to be entered during the workflow. Two files will be generated under ~/.ssh:

  • github_actions_deployer: private key
  • github_actions_deployer.pub: public key
  1. Copy the content of the public key into /home/deployer/.ssh/authorized_keys
  2. On Github repository, go to “Settings” and in “Secrets and variables”, create a Repository Secret named MYWEBSITE_COM_KEY for Actions, with the content of private key
  3. Add the user to the group that owns /var/www directory
1
2
sudo usermod -aG www-data deployer 
sudo chmod g+w /var/www

That’s it.