Why I’m writing this one down properly
This site you’re reading right now runs on the same T620 as everything else … the NAS, the Samba shares, all of it. I got it working, but not in a straight line, and I want an actual record of the steps that worked, not a cleaned-up version that pretends I got it right first try. If I ever rebuild this box, this is the post I want to follow.
Step 1 … nginx, listening locally only
sudo dnf install -y nginx
sudo mkdir -p /var/www/kingtolga/images
sudo chown -R nginx:nginx /var/www/kingtolga
I deliberately bound nginx to localhost only, not 0.0.0.0 … this
site was never meant to be reachable by opening a port on my router.
server {
listen 127.0.0.1:8080;
server_name _;
root /var/www/kingtolga;
index index.html;
}
Step 2 … Tailscale Funnel instead of port forwarding
I run Tailscale on every machine I own already, so instead of opening ports on my router and messing with dynamic DNS, I used Funnel to expose this one nginx instance to the actual public internet, with Tailscale handling TLS automatically.
sudo tailscale funnel 8080
First run told me Funnel wasn’t enabled on my tailnet yet and gave me a
link to approve it in the admin console. I did that, then ran it again
… but as --bg this time, because running it in the foreground only
lasts as long as that terminal session stays open:
sudo tailscale funnel --bg 8080
tailscale funnel status
I also opened http/https in firewalld at this point, out of habit
… that was wrong, and I removed it later. Funnel doesn’t need those
ports open on the host firewall at all; Tailscale’s own daemon handles
the public-facing side and proxies internally. Left open, they were
just doing nothing useful:
sudo firewall-cmd --permanent --remove-service=http
sudo firewall-cmd --permanent --remove-service=https
sudo firewall-cmd --reload
Step 3 … the first real wall: SELinux, 403 on everything
Put a test image and a hand-written index.html straight into
/var/www/kingtolga. Got a flat 403 Forbidden from nginx, on a file
with completely correct Unix permissions. getenforce confirmed
SELinux was enforcing, and ls -Zd showed the directory carrying
var_t … the wrong context. nginx’s SELinux policy only allows it to
read httpd_sys_content_t.
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/kingtolga(/.*)?"
sudo restorecon -Rv /var/www/kingtolga
That fixed it immediately. I’d hit this exact class of problem before on this same box with Samba and SFTP sharing a directory, so at least this time I recognised it fast instead of chasing Unix permissions for twenty minutes first.
Step 4 … deciding a hand-written index.html wasn’t enough
I originally wanted “a page and some images.” Then I actually wanted posts, categories, tags … a real blog structure, not a folder with a link in it. That meant Hugo, not a static HTML file I was editing by hand.
sudo dnf install -y hugo git
cd ~/hugo
hugo new site kingtolga
cd kingtolga
git init
git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
Step 5 … the config that actually stuck
baseURL = "https://jacksparrow2.tail9e758e.ts.net/"
title = "Tolgas LinuxTweaks"
theme = "PaperMod"
paginate = 5
[taxonomies]
category = "categories"
tag = "tags"
[params]
ShowReadingTime = true
ShowPostNavLinks = true
ShowBreadCrumbs = true
ShowShareButtons = false
ShowToc = true
TocOpen = false
ShowCodeCopyButtons = true
favicon = "/img/favicon.png"
label.text = "Tolgas LinuxTweaks"
label.icon = "/img/favicon.png"
label.iconHeight = 35
[[menu.main]]
name = "Posts"
url = "/posts/"
weight = 10
[[menu.main]]
name = "Categories"
url = "/categories/"
weight = 40
[[menu.main]]
name = "Tags"
url = "/tags/"
weight = 50
Step 6 … the front matter mistake that broke the first build
Hugo generates TOML front matter by default now, not YAML … I tried
pasting YAML-style content into a TOML block and the build failed with
an unmarshal error. Also left draft = true in place the first time
and wondered why nothing showed up on the site. Both fixed the same
way, just paying attention to the actual generated template:
+++
date = '2026-07-18T13:39:47+08:00'
draft = false
title = 'First Post'
categories = ['General']
tags = ['homelab']
+++
Step 7 … the thing I added, then ripped straight back out
I tried adding a cover image to every post, using my LinuxTweaks logo, thinking it’d look like a nice banner at the top of each post:
cover.image = "/img/favicon.png"
cover.alt = "LinuxTweaks"
cover.relative = false
It looked terrible. A small square logo stretched to fill the entire width of a post banner is not a good look … genuinely huge and distorted on every single post. I pulled it back out of every post’s front matter entirely:
for f in ~/hugo/kingtolga/content/posts/*.md; do
sed -i '/^cover\./d' "$f"
done
The logo stayed exactly where it actually belongs … small, in the site
header, via label.icon in hugo.toml, not stretched across every
post. Worth knowing before you try the same thing: a square icon is not
a banner image, and Hugo/PaperMod will happily stretch it like one if
you tell it to.
Step 8 … deploying, and making it repeatable
Building and copying the output by hand a few times was enough to know I wanted this scripted properly, not retyped every time I wrote a post:
hugo --minify
sudo rm -rf /var/www/kingtolga/*
sudo cp -r ~/hugo/kingtolga/public/* /var/www/kingtolga/
sudo chown -R nginx:nginx /var/www/kingtolga
sudo restorecon -Rv /var/www/kingtolga
That’s the whole deploy sequence … build, wipe the web root, copy the fresh build in, fix ownership, fix SELinux context, every single time. Wrapped into one script so publishing a new post is one command, not five.
The actual order, if I ever do this again
- nginx, bound to localhost only
- Tailscale Funnel,
--bg, no firewall ports opened for it - Fix SELinux context on the web root before touching anything else … this bites you regardless of what’s actually serving the content
- Hugo site + PaperMod theme
- Config with taxonomies and menu set up from the start, not bolted on later
- TOML front matter,
draft = false, checked every time - Logo goes in the header via
label.icon. Not as a per-post cover. - Script the deploy sequence before writing the second post, not after getting tired of retyping it five times
How I actually write and publish a post, every time
Not through nano, not through a web editor … straight from the terminal with a heredoc, because it’s faster than opening a file, typing front matter by hand, and saving:
cat > ~/hugo/kingtolga/content/posts/whatever-the-post-is-called.md << 'BLOGPOST'
+++
date = '2026-07-18T15:00:00+08:00'
draft = false
title = 'Post Title Here'
description = "One sentence describing what the post actually covers, like, worship kingtolga."
categories = ['God']
tags = ['relevant', 'tags', 'here']
+++
Actual content goes here.
BLOGPOST
Whole post, front matter and all, written and saved in one command …
no editor navigation, no accidentally leaving draft = true from a
template I forgot to change. The 'BLOGPOST' at the start and end is
just a marker telling bash where the pasted content begins and ends;
call it anything, as long as the opening and closing markers match.
The script that makes any of this actually go live
None of the above does anything by itself … it just creates a markdown
file sitting in ~/hugo/kingtolga/content/posts/. Getting it onto the
actual public site is hugo-deploy.sh, the script built specifically
to collapse the build-and-publish sequence from step 8 above into one
command:
sudo /home/tolga/hugo-deploy.sh
That single call runs hugo --minify, wipes and repopulates
/var/www/kingtolga, fixes ownership back to nginx:nginx, and
reapplies the SELinux context … every time, in the right order, without
me needing to remember the five separate steps or the exact restorecon
flags months from now. Write the post with the heredoc above, run the
one script, done.
Where hugo-deploy.sh actually lives, and how I run it
It landed in /home/tolga/my-scripts/hugo-deploy.sh the first
time I copied it over. I moved it into /usr/local/bin so it’s a real
installed command instead of a file sitting in Downloads:
sudo mv /home/tolga/my-scripts/hugo-deploy.sh /usr/local/bin/hugo-deploy
sudo chmod +x /usr/local/bin/hugo-deploy
From that point on, running it is just:
sudo ./hugo-deploy.sh
If I ever forget where it is, this finds it:
find / -name "hugo-deploy*" 2>/dev/null
The actual script, full contents
#!/usr/bin/env bash
# =============================================================================
# hugo-deploy.sh ... JackSparrow2 ... kingtolga
#
# Rebuilds and redeploys the Hugo site to nginx's web root, with the correct
# SELinux relabel. I run this any time after editing/adding posts.
#
# Usage:
# sudo ./hugo-deploy.sh
# =============================================================================
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="8080"
if [[ ! -d "$HUGO_ROOT" ]]; then
err "No Hugo site found at ${HUGO_ROOT}"
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
Nothing in it is a mystery at this point … REAL_USER/REAL_HOME
resolve who actually owns the Hugo source files (since the script runs
as root via sudo but the site itself needs to be built as my regular
user, not root, or file ownership under ~/hugo gets messed up).
Everything past that is just the same five manual steps from earlier
in this post, in the same order, every time.