About this website

This website was built using Nuxt paired with Nuxt Content for dynamic content (such as this post) and is currently hosted at my personal VPS on Digital Ocean.

These days platforms such as Vercel or Netlify are a much better and easier solution for hosting static content, they come with automatic CI/CD for example, but where's the fun in that, right?

My current method for publishing updates and new posts are literally these steps:

  1. pnpm generate
  2. rsync -avz .output/public/ $1@$2:<remote_path>

Quite rudimentary as you can see, but it works well enough. Change something, build locally and push with SSH.

So why add complexity for what should be easy and simple? For that I got a couple of answers:

  1. It's fun.
  2. I never implemented CD for static content, so I'll learn something new.
  3. I can publish new stuff directly from Github when away from my workstation.

Building a continuous delivery workflow for a static website

As mentioned above I'm familiar with building CD pipelines for back-end applications, but static content is new for me. A quick and dirty prompt to ChatGPT got me this workflow for Github Actions:

name: Deploy to Digital Ocean Droplet

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Install Node.js
        uses: actions/setup-node@v2
        with:
          node-version: "18.x"

      - name: Install dependencies
        run: npm install

      - name: Build static files
        run: npm run generate

      - name: Copy files to the server
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: "./dist/*"
          target: "/path/to/your/deployed/files"

      - name: Restart the web server (if needed)
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            sudo systemctl restart nginx

From the looks it looks simple enough, but I already see some points I'll have to touch, for example:

  • The name is quite bad.
  • I'm using Node.js 22, so guess I'll bump that to avoid any problems.
  • I'm using pnpm, not sure if corepack will be enabled by default.
  • The source directory is wrong.
  • I'm using Caddy as my webserver, not exactly sure if something like the last job will be necessary.

I'm writing this post as I setup this pipeline, so off to do some testing...

...some minutes later...

It did not work, of course, it never does. The first workflow I tried after some adjustments that I mentioned above:

name: Deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Install Node.js
        uses: actions/setup-node@v2
        with:
          node-version: "22.x"

      - name: Install dependencies
        run: pnpm install

      - name: Build static files
        run: pnpm generate

      - name: Copy files to the server
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: "./.output/*"
          target: "<remote_path>"

What went wrong:

  • pnpm was not available: /home/runner/work/_temp/fa719c57-ba34-4847-a12f-7f670af314be.sh: line 1: pnpm: command not found
  • SSH was eventually going to break as well because I removed it from port 22.

Off to do some research and try again...

...some minutes later...

It lives! Not on second try (never does), but it's working and this very post was published automagically. Way easier than I thought, took less than 40 minutes.

The final workflow:

name: Deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - uses: pnpm/action-setup@v4
        name: Install pnpm
        with:
          run_install: false

      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "22.x"
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build static files
        run: pnpm generate

      - name: Copy files to the server
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          port: ${{ secrets.REMOTE_PORT }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: "./.output/**"
          target: "<remote_path>"