Auto-starting a Hugo dev server at boot with systemd
I run my site’s Hugo dev server on a box on the LAN so I can preview drafts from any machine in the house. Starting it by hand after every reboot got old, so I moved it into a user-level systemd service.
The launcher script
The server itself is started by a small wrapper, ~/bin/hugo-run, which
binds to the machine’s LAN address instead of localhost:
#!/usr/bin/env bash
MY_ADDRESS=$(hostname -I | awk '{print $1}')
cd "$SITE_HOME" && \
hugo server --buildDrafts --disableFastRender --bind="$MY_ADDRESS" --baseURL="http://$MY_ADDRESS:1313"
Two details matter here:
--buildDraftsso unpublished posts show up.--disableFastRender, because fast render skips rebuilding pages that Hugo thinks haven’t changed, which bites you when editing theme layouts and partials.
It reads $SITE_HOME from the environment, so the unit file supplies it.
The unit file
Because everything lives under my home directory and needs no privileges, this is a
user service rather than a system one. No root, no User= directive, no permission
juggling.
~/.config/systemd/user/hugo-site.service:
[Unit]
Description=Hugo dev server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment=SITE_HOME=%h/src/mysite
WorkingDirectory=%h/src/mysite
ExecStart=%h/bin/hugo-run
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
Restart=on-failure with RestartSec=5 is doing real work, not just belt-and-braces:
the wrapper resolves the LAN address with hostname -I at launch. If the network isn’t
up yet, that comes back empty and Hugo fails to bind. systemd then retries every five
seconds until DHCP has handed out an address. (network-online.target isn’t reliably
available in the user manager, so the retry loop is the actual safety net.)
%h is systemd’s specifier for the user’s home directory — unit files don’t expand ~,
so use it rather than hardcoding an absolute path.
WantedBy=default.target is the user-session equivalent of multi-user.target — it’s
what gets pulled in when the user manager starts.
Enabling it
systemctl --user daemon-reload
systemctl --user enable --now hugo-site.service
systemctl --user status hugo-site.service
Check it’s actually serving:
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.1.42:1313/
The part that needs root: lingering
This is the step that’s easy to miss. A user service only runs while that user has an active session. Enable it, reboot, don’t log in, and nothing starts — which defeats the whole point on a headless box.
The fix is to enable lingering, which tells systemd to start the user manager at boot regardless of logins:
sudo loginctl enable-linger $USER
Verify:
loginctl show-user $USER | grep Linger
# Linger=yes
This is the one command in the whole setup that needs sudo.
Day-to-day
systemctl --user restart hugo-site # after layout/partial changes
systemctl --user stop hugo-site
journalctl --user -u hugo-site -f # follow the build log
That last one is handy: Hugo’s build errors and page counts land in the journal rather than a terminal you closed three days ago.