Work on your Hugo site from any machine on your network without syncing files or managing multiple copies. By storing your project on your Unraid server and mounting it as a network share, both your desktop and laptop point at the same source of truth.
Executive Summary#
This tutorial sets up a shared Hugo (a static site generator) development environment using an SMB (Server Message Block) share hosted on Unraid. SMB is the same file-sharing protocol used by Windows network drives. Your Hugo project lives on your Unraid server, and each machine on your network mounts that folder and accesses it directly. You will end up with three commands that handle your entire workflow: serve to start a local preview, publish to build and deploy your site, and restore to roll back a bad deploy.
Prerequisites#
| Component | Requirement |
|---|---|
| Unraid server | Running with a personal user share accessible over SMB |
| Pop!_OS machines | Two machines on the same LAN (Local Area Network) |
| Hugo | Version 0.157.0 extended or newer (installed on each machine) |
| VSCodium | Installed on each machine (Flatpak version supported) |
| Existing Hugo site | A working Hugo project ready to move to the share |
| Unraid credentials | Username and password for your Unraid share |
Step 1: Create the folder structure on Unraid#
SSH (Secure Shell) into your Unraid server as root. SSH is a way to run commands on a remote computer securely over a network.
ssh root@YOUR_UNRAID_IPCreate the sites folder inside your personal share. This folder will hold all of your Hugo projects, with room to add more sites later.
mkdir -p /mnt/user/YOUR_SHARE_NAME/sites/hofiverse
mkdir -p /mnt/user/YOUR_SHARE_NAME/sites/site2
mkdir -p /mnt/user/YOUR_SHARE_NAME/sites/backups/hofiverseThe -p flag tells the command to create any parent folders that do not exist yet. Exit the SSH session when done.
exitStep 2: Copy your existing Hugo project to the share#
From your primary machine, use scp (Secure Copy Protocol) to transfer your Hugo project to the new location on Unraid. The /. at the end of the source path copies the contents of the folder rather than nesting the folder itself inside the destination.
scp -r ~/your-site-folder/. root@YOUR_UNRAID_IP:/mnt/user/YOUR_SHARE_NAME/sites/hofiverseVerify the files landed correctly.
ssh root@YOUR_UNRAID_IP "ls /mnt/user/YOUR_SHARE_NAME/sites/hofiverse"You should see your Hugo project folders: archetypes, assets, content, layouts, static, themes, and so on.
Step 3: Set file permissions on the share#
Hugo needs write access to create a build lock file when the server starts. Set permissions on the project folder so your user can write to it over the network.
ssh root@YOUR_UNRAID_IP "chmod -R 777 /mnt/user/YOUR_SHARE_NAME/sites/hofiverse"Step 4: Install the site scripts on Unraid#
Three scripts handle the entire workflow. They live inside the project folder on the share so they are automatically available on every machine that mounts it.
Copy serve.sh, publish.sh, and restore.sh to the project folder on Unraid.
scp serve.sh root@YOUR_UNRAID_IP:/mnt/user/YOUR_SHARE_NAME/sites/hofiverse/
scp publish.sh root@YOUR_UNRAID_IP:/mnt/user/YOUR_SHARE_NAME/sites/hofiverse/
scp restore.sh root@YOUR_UNRAID_IP:/mnt/user/YOUR_SHARE_NAME/sites/hofiverse/The scripts are covered in detail in Step 8. You can find them at the bottom of this tutorial.
Step 5: Set up each machine#
Repeat these steps on every machine you want to develop from.
Install cifs-utils
cifs-utils is the package that allows Linux to mount SMB shares. CIFS (Common Internet File System) is the protocol that SMB is based on.
sudo apt install cifs-utilsCreate the mount point
A mount point is a folder on your local machine that acts as the entry point for the network share. Once mounted, anything you put in this folder is actually stored on Unraid.
sudo mkdir -p /mnt/sitesStore your Unraid credentials
Create a credentials file so your username and password are never stored in plain text in a system file. The chmod 600 command restricts the file so only root can read it.
sudo nano /etc/samba/unraid-credsAdd these two lines, replacing the values with your Unraid username and password.
username=YOUR_UNRAID_USERNAME
password=YOUR_UNRAID_PASSWORDsudo chmod 600 /etc/samba/unraid-creds
sudo chown root:root /etc/samba/unraid-credsAdd a sudoers rule for passwordless mounting
This rule allows your user to run the specific mount command without being prompted for your sudo password every time. sudoers is the file that controls what commands each user is allowed to run with elevated privileges.
sudo visudo -f /etc/sudoers.d/sites-mountAdd this line, replacing YOUR_USERNAME and the share path with your values.
YOUR_USERNAME ALL=(ALL) NOPASSWD: /usr/bin/mount -t cifs //YOUR_UNRAID_IP/YOUR_SHARE_NAME/sites /mnt/sites *Create the launcher script
Create a scripts folder in your home directory and add the launcher script there.
mkdir -p ~/scripts
nano ~/scripts/launcher.shPaste the following content.
#!/bin/bash
MOUNT_POINT="/mnt/sites"
SHARE="//YOUR_UNRAID_IP/YOUR_SHARE_NAME/sites"
CREDS="/etc/samba/unraid-creds"
SITE="${1:-hofiverse}"
SITE_PATH="$MOUNT_POINT/$SITE"
if ! mountpoint -q "$MOUNT_POINT"; then
echo "Mounting sites share..."
sudo mount -t cifs "$SHARE" "$MOUNT_POINT" \
-o credentials="$CREDS",uid=1000,gid=1000
if [ $? -ne 0 ]; then
echo "ERROR: Failed to mount share. Is your Unraid server reachable?"
exit 1
fi
echo "Mounted at $MOUNT_POINT"
fi
if [ ! -d "$SITE_PATH" ]; then
echo "ERROR: Site folder not found: $SITE_PATH"
echo "Available sites:"
ls "$MOUNT_POINT"
exit 1
fi
echo "Opening $SITE in VSCodium..."
flatpak run com.vscodium.codium "$SITE_PATH"Make it executable.
chmod +x ~/scripts/launcher.shAdd aliases to your shell
Aliases are shortcuts that let you type a short word instead of a full command. Add these to your ~/.bashrc file, which runs every time you open a terminal.
cat >> ~/.bashrc << 'EOF'
# Hugo site aliases
alias hofiverse="~/scripts/launcher.sh hofiverse"
alias site2="~/scripts/launcher.sh site2"
alias serve="/usr/local/bin/hugo server -D --poll 700ms --source /mnt/sites/hofiverse"
alias publish="bash /mnt/sites/hofiverse/publish.sh"
alias restore="bash /mnt/sites/hofiverse/restore.sh"
EOF
source ~/.bashrcThe --poll flag tells Hugo to check for file changes on a timer instead of relying on filesystem events. This is necessary because SMB shares do not always send filesystem change notifications correctly.
Step 6: Install the correct Hugo version#
The apt package manager often ships an older version of Hugo that may not be compatible with your theme. Install the correct version manually by downloading the .deb package directly from the Hugo GitHub releases page. A .deb file is a software installer for Debian-based Linux distributions like Pop!_OS and Ubuntu.
wget https://github.com/gohugoio/hugo/releases/download/v0.157.0/hugo_extended_0.157.0_linux-amd64.deb
sudo dpkg -i hugo_extended_0.157.0_linux-amd64.debVerify the installation.
hugo versionYou should see hugo v0.157.0 with +extended in the output. The extended version includes support for SCSS (a CSS preprocessor) which many Hugo themes require.
After installing, check the path Hugo was installed to, as it may differ between machines.
which hugoUpdate your serve alias in ~/.bashrc to match the path returned, then reload it.
source ~/.bashrcStep 7: Add the websites NFS mount for restore#
The restore script pushes files back to your production website folder on Unraid. That folder is exposed via NFS (Network File System), a different file-sharing protocol used for Unix-to-Unix connections. You need this mount on each machine for restore to work.
sudo mkdir -p /mnt/unraid/websites
sudo mount -t nfs YOUR_UNRAID_IP:/mnt/cache/websites /mnt/unraid/websitesTo make this mount persist after a reboot, add it to /etc/fstab. The fstab file tells your system which drives and network shares to mount automatically at startup.
echo "YOUR_UNRAID_IP:/mnt/cache/websites /mnt/unraid/websites nfs rw,relatime,vers=3,hard,proto=tcp,timeo=600,retrans=2,_netdev 0 0" | sudo tee -a /etc/fstabStep 8: The scripts#
These three scripts live inside your project folder on the share at /mnt/sites/hofiverse/. Because they are on the share, any machine that mounts it has access to them automatically.
serve.sh starts the Hugo development server with draft support and filesystem polling.
#!/bin/bash
SITE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if ! command -v hugo &>/dev/null; then
echo "ERROR: Hugo is not installed or not in PATH."
exit 1
fi
echo "Starting Hugo dev server..."
echo "Preview at: http://localhost:1313"
echo "Press Ctrl+C to stop."
echo ""
hugo server -D --poll 700ms --source "$SITE_DIR"publish.sh builds the site, backs up the current production version, and deploys the new build via rsync. It keeps the last 30 deploys as backups and rotates logs automatically. It also supports a --dry-run flag to simulate the deploy without making any changes.
#!/bin/bash
SITE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROD_DIR="/mnt/unraid/websites/hofiverse"
BACKUP_DIR="/mnt/sites/backups/hofiverse"
LOG_DIR="/mnt/sites/backups/hofiverse/logs"
MAX_BACKUPS=30
MAX_LOGS=10
MIN_DISK_MB=500
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOG_FILE="$LOG_DIR/deploy_$TIMESTAMP.log"
DRY_RUN=false
START_TIME=$(date +%s)
if [[ "$1" == "--dry-run" ]]; then
DRY_RUN=true
fi
mkdir -p "$LOG_DIR"
exec > >(tee -a "$LOG_FILE") 2>&1
echo "========================================"
echo "Deploy Script"
echo "Started: $(date)"
if $DRY_RUN; then
echo "MODE: DRY RUN (no changes will be made)"
fi
echo "========================================"
echo ""
echo "==> Running pre-flight checks..."
if ! command -v hugo &>/dev/null; then
echo "ERROR: Hugo is not installed or not in PATH."
exit 1
fi
if [ ! -d "$SITE_DIR" ]; then
echo "ERROR: Site directory not found: $SITE_DIR"
exit 1
fi
if [ ! -d "$PROD_DIR" ]; then
echo "ERROR: Prod directory not found: $PROD_DIR"
exit 1
fi
AVAILABLE_MB=$(df -m "$BACKUP_DIR" 2>/dev/null | awk 'NR==2 {print $4}')
if [ -z "$AVAILABLE_MB" ]; then
mkdir -p "$BACKUP_DIR"
AVAILABLE_MB=$(df -m "$BACKUP_DIR" | awk 'NR==2 {print $4}')
fi
if [ "$AVAILABLE_MB" -lt "$MIN_DISK_MB" ]; then
echo "ERROR: Not enough disk space. Available: ${AVAILABLE_MB}MB, Required: ${MIN_DISK_MB}MB"
exit 1
fi
echo " Hugo: OK"
echo " Site dir: OK ($SITE_DIR)"
echo " Prod dir: OK"
echo " Disk space: ${AVAILABLE_MB}MB available"
echo ""
echo "==> Building site..."
cd "$SITE_DIR" || { echo "ERROR: Could not cd into $SITE_DIR"; exit 1; }
if $DRY_RUN; then
echo " [DRY RUN] Would run: hugo --minify"
else
hugo --minify
if [ $? -ne 0 ]; then
echo "ERROR: Hugo build failed. Aborting."
exit 1
fi
FILE_COUNT=$(find "$SITE_DIR/public" -type f | wc -l)
echo " Built $FILE_COUNT files"
fi
echo ""
echo "==> Backing up current prod site..."
if $DRY_RUN; then
echo " [DRY RUN] Would backup $PROD_DIR to $BACKUP_DIR/site_$TIMESTAMP"
else
cp -r "$PROD_DIR" "$BACKUP_DIR/site_$TIMESTAMP"
if [ $? -ne 0 ]; then
echo "ERROR: Backup failed. Aborting."
exit 1
fi
echo " Backup created: site_$TIMESTAMP"
fi
echo ""
echo "==> Cleaning up old backups (keeping last $MAX_BACKUPS)..."
if $DRY_RUN; then
TO_DELETE=$(ls -dt "$BACKUP_DIR"/site_* 2>/dev/null | tail -n +$((MAX_BACKUPS + 1)))
if [ -z "$TO_DELETE" ]; then
echo " [DRY RUN] No backups to remove"
else
echo " [DRY RUN] Would remove:"
echo "$TO_DELETE" | while read -r line; do echo " $line"; done
fi
else
ls -dt "$BACKUP_DIR"/site_* 2>/dev/null | tail -n +$((MAX_BACKUPS + 1)) | xargs rm -rf
echo " Done"
fi
echo ""
echo "==> Cleaning up old logs (keeping last $MAX_LOGS)..."
if $DRY_RUN; then
echo " [DRY RUN] Would keep last $MAX_LOGS logs"
else
ls -dt "$LOG_DIR"/deploy_* 2>/dev/null | tail -n +$((MAX_LOGS + 1)) | xargs rm -f
echo " Done"
fi
echo ""
echo "==> Deploying to prod..."
if $DRY_RUN; then
echo " [DRY RUN] Would rsync $SITE_DIR/public/ to $PROD_DIR/"
rsync -av --delete --dry-run "$SITE_DIR/public/" "$PROD_DIR/"
else
rsync -av --delete --no-group --omit-dir-times --no-perms "$SITE_DIR/public/" "$PROD_DIR/"
if [ $? -ne 0 ]; then
echo "ERROR: rsync failed."
exit 1
fi
fi
END_TIME=$(date +%s)
ELAPSED=$((END_TIME - START_TIME))
echo ""
echo "========================================"
echo "Deploy Summary"
echo "========================================"
if $DRY_RUN; then
echo " Mode: DRY RUN"
else
echo " Mode: LIVE"
fi
echo " Status: SUCCESS"
echo " Files built: ${FILE_COUNT:-N/A}"
echo " Elapsed: ${ELAPSED}s"
echo " Log: $LOG_FILE"
echo " Completed: $(date)"
echo "========================================"restore.sh lists your available backups, prompts you to select one, confirms your choice, and rsyncs it back to production.
#!/bin/bash
BACKUP_DIR="/mnt/sites/backups/hofiverse"
PROD_DIR="/mnt/unraid/websites/hofiverse"
echo "========================================"
echo "Restore"
echo "========================================"
echo ""
BACKUPS=($(ls -dt "$BACKUP_DIR"/site_* 2>/dev/null))
if [ ${#BACKUPS[@]} -eq 0 ]; then
echo "ERROR: No backups found in $BACKUP_DIR"
exit 1
fi
echo "Available backups:"
echo ""
for i in "${!BACKUPS[@]}"; do
echo " [$i] $(basename ${BACKUPS[$i]})"
done
echo ""
read -p "Select backup number to restore: " SELECTION
if ! [[ "$SELECTION" =~ ^[0-9]+$ ]] || [ "$SELECTION" -ge "${#BACKUPS[@]}" ]; then
echo "ERROR: Invalid selection."
exit 1
fi
CHOSEN="${BACKUPS[$SELECTION]}"
echo ""
echo " Selected: $(basename $CHOSEN)"
echo " Target: $PROD_DIR"
echo ""
read -p "Are you sure? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "Aborted."
exit 0
fi
echo ""
echo "==> Restoring..."
rsync -av --delete --no-group --omit-dir-times --no-perms "$CHOSEN/" "$PROD_DIR/"
if [ $? -ne 0 ]; then
echo "ERROR: Restore failed."
exit 1
fi
echo ""
echo "========================================"
echo "Restore Complete"
echo " Restored from: $(basename $CHOSEN)"
echo " Completed: $(date)"
echo "========================================"Step 9: Test the full workflow#
Run through each command to confirm everything works end to end.
Mount and open VSCodium:
hofiverseStart the dev server in the same terminal:
serveOpen http://localhost:1313 in your browser. Edit a content file in VSCodium, save it, and refresh the browser to confirm changes appear.
Stop the server and do a dry run deploy:
publish --dry-runReview the output and confirm the paths and file counts look correct. Then run the live deploy.
publishTest restore:
restoreSelect the most recent backup, confirm with yes, and verify the site still loads correctly after the restore completes.
Lab Notes and Troubleshooting#
hofiverse: command not found after running machine-setup.sh The setup script runs as root and writes the launcher to /root/scripts/ instead of your home folder. Fix it manually: mkdir -p ~/scripts && sudo cp /root/scripts/launcher.sh ~/scripts/ && sudo chown $USER:$USER ~/scripts/launcher.sh && chmod +x ~/scripts/launcher.sh. Then add the aliases manually to ~/.bashrc and run source ~/.bashrc.
codium: command not found in the launcher VSCodium is installed as a Flatpak, which uses a different command. Update the launcher: sed -i 's|codium "$SITE_PATH"|flatpak run com.vscodium.codium "$SITE_PATH"|' ~/scripts/launcher.sh.
mount error(13): Permission denied The credentials file has the wrong username or password, or was written with the root account instead of your Unraid user. Edit /etc/samba/unraid-creds and confirm the username and password match your Unraid share credentials, not the root account.
Hugo build error: function "try" not defined The version of Hugo installed via apt is too old for your theme. Remove it with sudo apt remove hugo and install the correct version manually using the .deb package from the Hugo GitHub releases page.
serve alias uses wrong Hugo path Hugo installs to /usr/local/bin/hugo when installed via .deb but /usr/bin/hugo when installed via apt. Run which hugo to find the correct path and update the alias in ~/.bashrc to match.
rsync: chgrp failed: Operation not permitted This is a harmless NFS permission warning. The files transfer successfully. The --no-group, --omit-dir-times, and --no-perms flags in the rsync command suppress this error.
restore reports no backups found The backup directory path in restore.sh does not match where publish.sh is writing backups. Confirm both scripts use the same BACKUP_DIR value.
Live reload does not fire in the browser This is expected behavior over SMB. Hugo’s filesystem watcher relies on kernel events that SMB does not always deliver. The --poll flag works around this by checking for changes on a timer. Save your file, watch for the rebuild message in the terminal, then refresh the browser manually.
Summary#
Your Hugo project now lives on your Unraid server at /mnt/user/YOUR_SHARE_NAME/sites/hofiverse. Both machines mount that share at /mnt/sites and work from the same files. Hugo runs locally on whichever machine you are sitting at, and the three scripts handle previewing, deploying, and rolling back your site.
Your daily workflow from any machine is three commands:
hofiverse # mount the share and open VSCodium
serve # start the Hugo dev server at localhost:1313
publish # build, back up, and deploy the siteTo add a second site in the future, create a new folder under /mnt/user/YOUR_SHARE_NAME/sites/, copy the scripts into it with updated paths, and add a new alias to ~/.bashrc.