← Back to posts

Automating blog deployment with GitHub Actions

Every time I published a new post I had to do the same thing: SSH into the server, navigate to the right folder, run hugo build, and disconnect. It worked, but it was manual work that didn’t need to be manual.

GitHub Actions fixes that. Now every time I push a change to GitHub, the deployment happens automatically — no SSH, no manual commands, nothing.

Here’s how I set it up.


What GitHub Actions actually is

GitHub Actions is a CI/CD tool built into GitHub. You define a workflow in a YAML file and GitHub runs it automatically when something happens — in this case, when I push new content.

The workflow does three things:

  1. Detects a push to the main branch
  2. SSHs into the AWS VM
  3. Runs git pull and hugo build

That’s it. Simple, but it removes the manual step completely.


Step 1 — Create a dedicated SSH key for GitHub Actions

Instead of using my personal SSH key, I created a separate one specifically for this:

ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github_actions -N ""
cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys

The public key stays on the server (authorized_keys). The private key goes to GitHub. If it ever gets compromised, I can revoke it without touching my personal access.


Step 2 — Add the public key to GitHub

Go to github.com → Settings → SSH and GPG keys → New SSH key and paste the public key:

cat ~/.ssh/github_actions.pub

This allows the VM to authenticate with GitHub using SSH instead of a username and password.


Step 3 — Configure SSH on the VM

Tell git to use this specific key when connecting to GitHub:

cat > ~/.ssh/config << 'EOF'
Host github.com
  IdentityFile ~/.ssh/github_actions
  StrictHostKeyChecking no
EOF

chmod 600 ~/.ssh/config

Without this step, git pull inside the workflow would either fail silently or prompt for credentials — neither of which works in an automated pipeline.


Step 4 — Store deployment credentials as GitHub secrets

In the repository settings under Secrets and variables → Actions, add three secrets:

These are encrypted and never exposed in logs.


Step 5 — Create the workflow file

name: Deploy Blog

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to VM
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd ~/lucianosblog
            git pull origin main
            hugo build

This file lives at .github/workflows/deploy.yml. Every push to main triggers it.


Three things that caught me out

Token permissions — when I pushed the workflow file, GitHub rejected it:

refusing to allow a Personal Access Token to create or update workflow
without `workflow` scope

Fix: regenerate the token and add the workflow scope alongside repo.

Security Group — GitHub Actions connects from external IPs, so port 22 needs to be open to 0.0.0.0/0 in the AWS Security Group. I only had my own IP whitelisted, which caused a timeout error.

Silent git pull failure — the workflow showed green but nothing deployed. The issue was that git pull was using HTTPS and had no credentials to authenticate. Switching to SSH (Steps 2 and 3) fixed it.


The result

The publishing workflow is now:

git add .
git commit -m "New post"
git push

30 seconds later the site is live. No SSH, no manual build, no context switching.

The entire blog also lives in GitHub — content, theme, config, everything. If the server goes down, redeploying on a new instance takes minutes.


Questions or feedback? Connect with me on LinkedIn.