Skip to main content
  1. Projects/

Automating hofiverse.com Deployments with a Bash Script

Status Stable
Cost $0
Time Spent ~2 hours
Outcome

Reduced deploy time to 4 seconds with automatic backups and rollback support

Stack
Bash Hugo rsync NFS Unraid

Every time I made changes to hofiverse.com I was manually running hugo build and copying files over to my Unraid server. It worked, but it was tedious and error prone. I decided to write a deploy script that handles the whole process with a single command.

The Problem
#

My workflow before the script looked like this:

  1. Make changes locally
  2. Run hugo --minify to build the site
  3. Manually rsync the output to /mnt/user/websites/hofiverse on my Unraid server
  4. Hope I did not forget a step

There was no backup of the previous site before deploying, no log of what happened, and no safety net if something went wrong mid-deploy.

The Goal
#

A single command that handles the entire deploy reliably. Before writing anything I mapped out what the script needed to do:

  • Run a Hugo build first and abort if it fails
  • Back up the current prod site before overwriting it
  • Keep only the last 3 backups and clean up older ones automatically
  • Rotate logs so they do not pile up indefinitely
  • Check for disk space before attempting a backup
  • Show a clean summary at the end with elapsed time and file count
  • Support a dry run mode for testing without making any changes

The Build
#

The Investigation
#

The site lives on my laptop at ~/hofiverse-site. The production files are served by a Caddy container on my Unraid server from /mnt/user/websites/hofiverse. Backups and logs go to /mnt/user/backup/hofiverse-site.

To deploy from my laptop I needed the Unraid shares mounted over NFS. I added NFS exports for both the websites and backup shares in the Unraid UI, then mounted them on my laptop:

sudo mkdir -p /mnt/unraid/websites
sudo mkdir -p /mnt/unraid/backup
sudo mount -t nfs YOUR-UNRAID-IP:/mnt/user/websites /mnt/unraid/websites
sudo mount -t nfs YOUR-UNRAID-IP:/mnt/user/backup /mnt/unraid/backup

With those mounted, the script can read and write to the Unraid server as if they were local directories.

The Pivot
#

The first version of the script had no pre-flight checks. It would attempt the Hugo build, and if that failed it would still try to deploy whatever was in the public/ folder from the last successful build. That meant a failed build could silently deploy stale content.

The fix was making the build step a hard gate. If Hugo exits with a non-zero code, the script stops immediately before touching anything in production.

The Fix
#

The full script lives at ~/deploy.sh. Here is how each section works.

Pre-flight checks

Before doing anything destructive the script verifies that Hugo is installed, both directories exist, and there is enough free disk space on the backup share:

if ! command -v hugo &>/dev/null; then
    echo "ERROR: Hugo is not installed or not in PATH."
    exit 1
fi

Build

hugo --minify
if [ $? -ne 0 ]; then
    echo "ERROR: Hugo build failed. Aborting."
    exit 1
fi

Backup and rotation

Before deploying, the current prod directory is copied to the backup share with a timestamp in the folder name:

cp -r "$PROD_DIR" "$BACKUP_DIR/hofiverse_$TIMESTAMP"

After the backup is created, the script trims any backups beyond the last 3:

ls -dt "$BACKUP_DIR"/hofiverse_* | tail -n +4 | xargs rm -rf

The same pattern applies to log files, keeping only the last 10.

Deploy

rsync -av --delete "$SITE_DIR/public/" "$PROD_DIR/"

The --delete flag removes any files from prod that no longer exist in the build output, keeping the production directory clean.

Dry run mode

Passing --dry-run makes the script simulate every step without writing anything. Hugo still runs so build errors surface, but the backup, cleanup, and rsync all print what they would do instead of doing it:

~/deploy.sh --dry-run

Summary

At the end of every run the script prints a summary:

========================================
Deploy Summary
========================================
  Mode:        LIVE
  Status:      SUCCESS
  Files built: 47
  Elapsed:     4s
  Log:         /mnt/unraid/backup/hofiverse-site/logs/deploy_20260320_091223.log
  Completed:   Fri Mar 20 09:12:27 AM MST 2026
========================================

Results
#

The whole process now takes one command and about 4 seconds. The last 3 versions of the site are always available if I need to roll back, and every deploy leaves a log I can check if something looked off.

What I Would Do Differently
#

The NFS mounts need to be active before the script runs or it will fail silently trying to write to an unmounted path. I worked around this by adding the mounts to /etc/fstab with the _netdev flag, but a better solution would be having the script detect whether the mounts are active and either mount them automatically or exit with a clear error message.

The backup step also copies the entire production directory every time regardless of what changed. For a small static site this is fine, but an incremental backup using rsync snapshots would be more efficient and use less disk space over time.

What Is Next
#

The script currently requires the NFS mounts to be set up manually on any new machine. Packaging the full setup, including the mount configuration and the script itself, into a bootstrap script would make it portable across machines without any manual steps.


Disclaimer: Content on this site is provided as-is with no guarantees of accuracy, completeness, or fitness for any particular purpose. Always test in a safe, non-production environment before applying anything documented here to systems you rely on. I am not responsible for data loss, damage, or security issues resulting from following these guides. Software and services change over time and steps that worked when this was written may not work in the future.