How and why

I’ve been building simple websites manually since the Geocities era. As a matter of fact, this URL used to host one before this blog was made, in all of its’ 90s glory. I also had some blogs before this one, which were not tech related, and which used standard dynamic platforms such as Wordpress or Blogger - also built a few of those for some clients back in the day.

The server this is running on is my cloud lab, a tiny and cheap VM somewhere in the cloud. I wanted something efficient, yet modern, and which would not take too much of my time apart from writing content. So I decided to experiment with static website generators, and I chose Hugo.

Of course, days of manually uploading FTP to the server are beyond us. Apart from my public Github, I also host a copy of Gitea, as a VCS platform for this codebase, and a full devops platform. I’m running a self-hosted runner for CI/CD pipelines, and so far I built a simple one that deploys this site, and iterated on it to include the full build. Might be an overkill for a few HTML and CSS files, but nearly instant deployment on commit to main is convenient, and mainly what DevOps is about.

Pipeline implementation no.1

Super simple Github/Gitea pipeline to deploy prebuilt static files via scp, all parameters are injected as secrets and target server should be accessible by runner image:

name: Copy files with SCP

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Setup SSH
      run: |
        mkdir -p ~/.ssh
        echo "$SSH_KEY" > ~/.ssh/id_rsa
        chmod 600 ~/.ssh/id_rsa
        ssh-keyscan -H $REMOTE_SERVER_IP >> ~/.ssh/known_hosts
      env:
        REMOTE_SERVER_IP: ${{ secrets.DESTINATION_HOST }}
        SSH_KEY: ${{ secrets.SSH_KEY }}

    - name: Copy file with SCP
      run: |
        scp -r -i ~/.ssh/id_rsa $LOCAL_PATH/* $REMOTE_USER@$REMOTE_SERVER_IP:$REMOTE_PATH/
      env:
        REMOTE_SERVER_IP: ${{ secrets.DESTINATION_HOST }}
        REMOTE_USER: ${{ secrets.SSH_USERNAME }}
        REMOTE_PATH: ${{ secrets.DESTINATION_FOLDER }}
        LOCAL_PATH: ${{ secrets.SOURCE_FOLDER }}

Pipeline implementation no.2

While the above pipeline might be useful for you if you just need to copy some files to your destination, we can do better for the specific project of this website (or similar ones) by including the build step itself. Let’s put all the ENV variables in one place, and clear the destination directory before deploying the new files - I wouldn’t do this on an important production, but there are no visits to this website yet so I don’t really mind a second or two of downtime. Here’s the expanded, improved and cleaned up version of the pipeline that I actually use to deploy this site:

name: Build a Hugo site and copy final files with SCP

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      REMOTE_SERVER_IP: ${{ secrets.DESTINATION_HOST }}
      REMOTE_USER: ${{ secrets.SSH_USERNAME }}
      REMOTE_PATH: ${{ secrets.DESTINATION_FOLDER }}
      LOCAL_PATH: ${{ secrets.SOURCE_FOLDER }}
      SSH_KEY: ${{ secrets.SSH_KEY }}
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Setup Hugo
      run: |
        curl -L -o /tmp/hugo.tar.gz 'https://github.com/gohugoio/hugo/releases/download/v0.146.3/hugo_0.146.3_Linux-64bit.tar.gz'
        tar -C ${RUNNER_TEMP} -zxvf /tmp/hugo.tar.gz hugo

    - name: Build
      run: ${RUNNER_TEMP}/hugo

    - name: Setup SSH
      run: |
        mkdir -p ~/.ssh
        echo "$SSH_KEY" > ~/.ssh/id_rsa
        chmod 600 ~/.ssh/id_rsa
        ssh-keyscan -H $REMOTE_SERVER_IP >> ~/.ssh/known_hosts

    - name: Clean remote directory
      run: |
        ssh -i ~/.ssh/id_rsa ${REMOTE_USER}@${REMOTE_SERVER_IP} \
          "rm -rf ${REMOTE_PATH}/*"

    - name: Copy website with SCP
      run: |
        scp -r -i ~/.ssh/id_rsa $LOCAL_PATH/* $REMOTE_USER@$REMOTE_SERVER_IP:$REMOTE_PATH/

And boom, uploading the source to my repository automatically triggers this pipeline, builds and refreshes this site. Easier than using the CMS, at least for those used to a Git workflow.

Final notes

I went with using a lot of secrets here, some of them such as folder paths are safe to use as variables instead. As always, be careful when using rm -rf and make sure your paths are correct.