Why This Script Exists
Every time I write a new blog post, Hugo has to rebuild the entire site. That means recompiling all the markdown, regenerating all the HTML, copying files to the web root where nginx can serve them, and fixing Linux file permissions so nginx can actually read them.
Doing all that manually is tedious. Running separate commands every time is error-prone. I wanted one command that does everything.
So I created hugo-deploy.
What The Script Does (Simply)
The script does five things in order.
1. Check you are root
The script needs to write to system directories like /var/www/. Only root can do that. If you try to run it as a regular user, it exits with an error and tells you to use sudo.
2. Figure out who you really are
When you run sudo hugo-deploy, the script is running as root, but it needs to know who the real user is (me, tolga). It reads the SUDO_USER environment variable to find out. Then it gets my home directory from /etc/passwd.
Why? Because Hugo needs to build as the real user, not as root. If Hugo runs as root, it creates files owned by root, and that causes permission problems later.
3. Build the Hugo site
Hugo reads all my markdown posts, generates HTML, and puts it in the public/ folder. The script runs this command:
hugo --minify
The --minify flag makes the HTML and CSS smaller so pages load faster. If Hugo fails at this step, the script stops and doesn’t deploy anything. Safety first.
4. Deploy to the web root
The web root is where nginx looks for files to serve. I set it to /var/www/kingtolga/. The script does:
- Delete everything in the old web root
- Copy all the new HTML from Hugo’s public/ folder
- Change ownership to nginx:nginx so nginx can read the files
- Run restorecon to fix SELinux labels (Fedora’s security system)
Without the ownership and SELinux step, nginx wouldn’t be able to read the files and would return permission denied errors.
5. Verify the site is actually live
The script waits one second, then tries to curl the site on localhost:80. If it gets a 200 OK response, it prints success. If it gets nothing, it prints the command you need to run to debug it.
Why I Put It in /usr/local/bin/
I could have put the script anywhere. I chose /usr/local/bin/ for specific reasons.
/usr/local/bin/ is in the system PATH
When you type a command in the terminal, the shell searches a list of directories (called PATH) to find the executable. /usr/local/bin/ is always in that PATH. That means I can run sudo hugo-deploy from anywhere on the system without typing the full path. If the script was in /root/ or /opt/ or my home directory, I would have to type the full path every time.
/usr/local/ survives package updates
Fedora and other Linux distros own /usr/bin/. When you update packages, they can add or replace files in /usr/bin/. But /usr/local/ is reserved for the system administrator (that’s me). Package managers never touch it. So my script will never get overwritten or deleted by an update.
/usr/local/ is the Linux standard for local scripts
The Filesystem Hierarchy Standard (FHS) says:
“Binaries in /usr/local/bin/ are meant for programs that are not managed by the package manager.”
That’s exactly what hugo-deploy is. It’s a custom script I wrote, not something from Fedora’s package manager. Putting it in /usr/local/bin/ follows the standard and makes my system predictable.
/usr/local/bin/ feels like the right place
When I ls /usr/local/bin/, I see custom tools. My backup scripts live there. Any utility I write goes there. It’s like a toolbox. System packages go in /usr/bin/ (owned by the distro), and my tools go in /usr/local/bin/ (owned by me).
How I Set It Up
I copied the script to /usr/local/bin/ and made it executable:
sudo cp hugo-deploy.sh /usr/local/bin/hugo-deploy
sudo chmod 755 /usr/local/bin/hugo-deploy
Note that I removed the .sh extension. On Unix systems, executables usually don’t have file extensions. The extension is just for humans to know what language it’s written in. The system doesn’t care.
Now I can deploy by typing:
sudo hugo-deploy
From anywhere on T620. No path needed. No .sh extension. Just the command.
The Script Itself
Here’s what it looks like:
#!/usr/bin/env bash
set -uo pipefail
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
info() { echo -e "${YELLOW}[*]${NC} $1"; }
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
err() { echo -e "${RED}[!]${NC} $1"; }
if [[ $EUID -ne 0 ]]; then
err "Run as root: sudo $0"
exit 1
fi
REAL_USER="${SUDO_USER:-tolga}"
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
HUGO_ROOT="${REAL_HOME}/hugo/kingtolga"
WEB_ROOT="/var/www/kingtolga"
NGINX_PORT="80"
if [[ ! -d "$HUGO_ROOT" ]]; then
err "No Hugo site found at ${HUGO_ROOT} — run install-hugo-site.sh first"
exit 1
fi
info "Building site..."
su "$REAL_USER" -c "cd '$HUGO_ROOT' && hugo --minify" \
&& ok "Build succeeded" \
|| { err "hugo build failed — nothing was deployed"; exit 1; }
info "Deploying to ${WEB_ROOT}..."
rm -rf "${WEB_ROOT:?}"/*
cp -r "${HUGO_ROOT}/public/"* "$WEB_ROOT/"
chown -R nginx:nginx "$WEB_ROOT"
restorecon -Rv "$WEB_ROOT" > /dev/null
ok "Deployed"
info "Verifying..."
sleep 1
if curl -sf "http://127.0.0.1:${NGINX_PORT}/" > /dev/null; then
ok "Site is live and responding locally"
else
err "Local check failed — run: curl -I http://127.0.0.1:${NGINX_PORT}/"
fi
The set -uo pipefail line makes bash strict. It exits immediately if any command fails. That prevents me from accidentally deploying a broken build.
The su "$REAL_USER" -c line switches to the real user to build Hugo, then switches back to root to deploy. This is important for file ownership.
Why This Matters
Before I had this script, deploying a new post took five commands:
cd ~/hugo/kingtolga
hugo --minify
sudo rm -rf /var/www/kingtolga/*
sudo cp -r public/* /var/www/kingtolga/
sudo chown -R nginx:nginx /var/www/kingtolga
If I forgot one step, the site might not update. If I typo’d a path, bad things could happen. If nginx lost permissions, the site would 404.
Now I type one command:
sudo hugo-deploy
The script does all five things automatically, in the right order, with safety checks. If anything fails, it tells me what went wrong instead of silently breaking things.
That’s the whole point of good infrastructure. Automate the routine. Make it so simple that you can’t get it wrong.
The Lesson
Small scripts in the right places save time and prevent mistakes. /usr/local/bin/ is where they belong on a Linux system.
Next time you find yourself typing the same sequence of commands over and over, turn it into a script. Put it in /usr/local/bin/. Make it executable. Stop typing the long version.
That’s how you build a system that works for you instead of against you.
-Tolga