Hugo
Overview
This guide covers the deployment and use of Hugo on Ubuntu 24.04 on Azure, using SecureAnyCloud Azure Marketplace images. Hugo is the leading open source static site generator. It takes Markdown content and HTML templates and renders them into a complete static website, extremely fast, from a single Go binary.
A static site has no database, no application runtime and no request-time rendering. There is nothing to keep patched behind the web server and nothing to fall over under load. The trade-off is that something has to build the site and something has to serve it. That is exactly what this image does.
This is a build-and-serve appliance, not a bare command line tool. Hugo on its own is a CLI. This image is the whole loop: Hugo installed system-wide, a complete working starter site on disk, nginx serving the generated output on ports 80 and 443, and a documented publish path that turns an edit into a live page. When the VM finishes booting, point a browser at its public address. You get a real, styled website, not a directory listing, an nginx welcome page or an error.
The image ships the extended edition, and it matters. Hugo ships in two builds. The extended build embeds the SCSS/Sass transpiler, which a large fraction of Hugo themes require. On a plain build, those themes fail with this feature is not available in your current Hugo version.
The proof is functional rather than cosmetic. The starter site's own stylesheet is real SCSS compiled through Hugo Pipes, so the site could not render at all on a non-extended binary. Every successful publish on this appliance demonstrates the transpiler working.
The starter site is documentation, not filler. On first boot, the pages the appliance serves are about the appliance. They explain how to add a page, add a section, change the look, publish, and swap in a third-party theme instead.
SecureAnyCloud wrote the templates and stylesheet, so no third-party theme is vendored. There is no upstream theme licence to carry and nothing that can go stale underneath you. Delete all of it and put your own site there. Nothing in layouts/ is load-bearing.
Publishing cannot take your site down. hugo-publish renders into a staging directory, checks that the result is a real site, and only then swaps it into place. A syntax error in a template or in hugo.toml makes the command fail loudly with the error, while the previously published site keeps serving. The command also reads Hugo's own output rather than trusting its exit status, so a build that reports success while emitting errors is still treated as a failure.
There is no login, and that is deliberate. A static site generator is a build tool. Someone who already holds shell access edits the content over SSH, and the site it generates is public by definition. So this appliance ships no admin panel, no login and no upload form. There is no application credential to rotate and nothing authenticated exposed to the internet. The Hugo development server, which binds a live reload listener and watches the filesystem, is never installed as a service. nginx serves pre-rendered bytes only.
Each instance gets its own TLS certificate, and no key ships in the image. The HTTPS listener needs a certificate. Baking one in would put the same private key on every customer VM, so the image ships no certificate and no key at all. Both are generated on your VM at first boot, and nginx is gated so that it cannot serve before that has happened.
What you get:
- The official Hugo 0.164.0 extended release binary, pinned by SHA-256 and redistributed unmodified, at
/usr/local/bin/hugo - A complete working starter site at /var/www/hugo, with content, templates and an SCSS stylesheet, all authored by SecureAnyCloud
- nginx serving the generated site on 80 and 443, with a per-instance self-signed certificate generated at first boot
hugo-publish, one command that re-renders the site and swaps it in atomicallyhugo-publish.pathandhugo-publish.timer, systemd watchers that republish your edits without you running a commandhugo-selftest, a shipped command that proves the binary is extended, that the site re-renders, that nginx serves exactly those bytes, and that every asset the page references resolves
Prerequisites
- An active Azure subscription, an SSH public key, and a VNet with a subnet in the target region.
- A subscription to the Hugo listing on Azure Marketplace.
- Optionally, a domain name you can point at the VM. The appliance works on its raw public IP, and you can add a domain later.
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is a comfortable starting point. Hugo renders the bundled site in well under a second, and nginx serving static files is close to free.
Very large sites (tens of thousands of pages) render faster on more cores. Move to a compute-optimised size such as Standard_F4s_v2 if your build time matters.
Deploy from the Azure Portal
Search for Hugo in the Marketplace, select the SecureAnyCloud publisher, and select Create.
Configure these NSG rules:
- TCP 80 and TCP 443 from the networks that should reach the website. For a public site, which is the normal case for a static site, that is the whole internet.
- TCP 22 for admin SSH from your management network only.
Deploy from the Azure CLI
az vm image terms accept --urn sac1647283583153:hugo:default:latest
az vm create \
--resource-group my-hugo-rg \
--name my-hugo-vm \
--image sac1647283583153:hugo:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
# Open the website to the networks that should see it
az vm open-port --resource-group my-hugo-rg --name my-hugo-vm --port 80 --priority 1010
az vm open-port --resource-group my-hugo-rg --name my-hugo-vm --port 443 --priority 1020Connect and confirm the site is being served
Log in over SSH as azureuser. A few seconds after the VM reports ready, first boot resolves this VM's public address, writes it into the site configuration, generates the TLS certificate and renders the site.
sudo systemctl is-active nginx.service hugo versionExpected output:
active hugo v0.164.0-ce2470e7012b5ab5fc4e10ebe4027e9f8d9e00dc+extended linux/amd64 BuildDate=2026-07-06T16:39:30Z VendorInfo=gohugoioCheck for the +extended suffix. It tells you the embedded SCSS transpiler is present, so SCSS-based themes build.
Confirm that nginx holds both website ports and that the automatic publish watchers are running:
sudo ss -lntp | grep -E ':(80|443)\b' systemctl is-active hugo-publish.timer hugo-publish.pathThe image records exactly which upstream artifact it shipped, including the pinned digest:
cat /usr/local/share/hugo/VERSIONLook at the site
Open http://<public-ip>/ in a browser. HTTPS on the same address works too. Your browser warns once about the self-signed certificate. Step 8 replaces it with your own.
The styling on that page is evidence, not decoration. The stylesheet does not exist as a .css file anywhere on disk. Hugo's embedded transpiler compiles it from assets/scss/main.scss every time the site is published. A page that renders styled was built by the extended edition.
The Posts link shows the section index. Every entry is one Markdown file in content/posts/.
An individual post shows what Hugo does with Markdown. Headings, prose, inline code and fenced code blocks all become real HTML.
From the VM itself, confirm that what nginx serves is Hugo's output:
curl -s http://127.0.0.1/ | grep -oE 'content="Hugo [0-9.]+"'Expected output:
content="Hugo 0.164.0"Where the site lives
Everything is under /var/www/hugo, owned by an unprivileged hugo account.
ls -1 /var/www/hugo find /var/www/hugo/content -name '*.md' | sort- content/: your Markdown, one file per page
- layouts/: the HTML templates (baseof.html is the page shell)
assets/scss/main.scss: the stylesheet source, compiled at publish time- static/: files copied to the site verbatim (images, downloads)
- public/: the generated site, which is nginx's document root
hugo.toml: site configuration, including baseURL
The site source sits one level above nginx's document root, so your Markdown, templates and configuration are not reachable over HTTP.
First boot writes this VM's own endpoint summary. Only root can read it, and it spells out the paths you will use:
sudo cat /root/hugo-endpoints.txtPublish your first change
Add a page, publish, and it is live. That is the whole loop.
sudo tee /var/www/hugo/content/posts/my-first-post.md >/dev/null <<'EOF' --- title: "My first post" date: 2026-02-01 --- Written in Markdown, rendered by Hugo, served by nginx. EOF
sudo hugo-publishExpected output:
[...] hugo-publish: change detected: /var/www/hugo/content/posts [...] hugo-publish: published 12 page(s), 104K on disk, into /var/www/hugo/publicConfirm the page is live:
curl -sf http://127.0.0.1/posts/my-first-post/ | grep -oF 'My first post'In day-to-day use, you edit with an editor rather than a heredoc. sudo -e opens your $EDITOR on a copy and installs it when you save:
sudo -e /var/www/hugo/content/posts/my-first-post.md
sudo hugo-publishHugo can also scaffold the front matter for you:
cd /var/www/hugo && sudo -u hugo hugo new content posts/another-post.mdThat command writes the file with draft = true set. Delete that line, or publish drafts with hugo --buildDrafts, and the page goes live.
A broken edit will not take your site down. hugo-publish renders to a staging directory and swaps it in only once the render has produced a real site. If you leave a template half-finished, the command fails with the error and the previous site keeps serving. Fix the file and publish again.
Automatic publishing
You do not have to remember hugo-publish. Two systemd units watch for changes, and both trigger the same hugo-publish.service:
hugo-publish.pathreacts within seconds to a change directly undercontent/or tohugo.toml.hugo-publish.timersweeps every minute. This catches edits deeper in the tree, such ascontent/posts/,assets/scss/andlayouts/.
hugo-publish skips the render entirely when nothing has changed, so an idle VM does no work. A lock serialises concurrent publishes, so a sweep that fires while you run the command by hand is harmless.
To try it, edit a file and run nothing at all:
sudo tee -a /var/www/hugo/content/posts/my-first-post.md >/dev/null <<'EOF' This paragraph was added without running any publish command. EOF for i in $(seq 1 30); do page="$(curl -s http://127.0.0.1/posts/my-first-post/ || true)" case "${page}" in *"without running any publish command"*) break ;; esac sleep 5 done curl -sf http://127.0.0.1/posts/my-first-post/ | grep -oF 'without running any publish command'Watch the publishes as they happen:
sudo journalctl -u hugo-publish.service -n 20 --no-pagerTo publish only on demand, disable the watchers. hugo-publish still works by hand:
sudo systemctl disable --now hugo-publish.path hugo-publish.timerPoint a domain at it
Two things change when you move from the raw IP to a domain: the baseURL Hugo bakes into absolute links, and the TLS certificate nginx presents.
Ordinary navigation and asset links on this site are relative, so they already work over HTTP, over HTTPS and behind any hostname. Hugo uses baseURL for the canonical tag, the RSS feed and the sitemap, so set it to your real address:
sudo -e /var/www/hugo/hugo.toml
# set: baseURL = "https://<your-domain>/"
sudo hugo-publishThen install a certificate for that domain. The appliance reads its certificate from a fixed pair of paths, so you only need to replace the files and reload nginx:
# Replace with your own certificate and key for <your-domain>
sudo install -m 0644 -o root -g root fullchain.pem /etc/nginx/tls/sac-hugo.crt
sudo install -m 0600 -o root -g root privkey.pem /etc/nginx/tls/sac-hugo.key
sudo nginx -t && sudo systemctl reload nginxFor an automatically renewed certificate, install certbot and point it at the same paths. Alternatively, terminate TLS at an Azure Application Gateway or Front Door in front of the VM.
Make it your own site
The starter content, templates and stylesheet are all yours to change or delete.
- Content: remove everything in
content/and write your own. A directory undercontent/becomes a section. For example,content/notes/serves at/notes/and gets an index page fromlayouts/section.html. - Look:
assets/scss/main.scssis real SCSS with variables, nesting and mixins. Change$navyor$accentat the top and publish. The transpiler recompiles it. - Templates:
layouts/baseof.htmlis the page shell.home.html,page.htmlandsection.htmlfill in itsmainblock.
To use a third-party theme instead of the bundled templates, clone it into themes/, name it in the configuration, and remove layouts/ so the theme's own templates take over. Because this image is the extended edition, SCSS-based themes build correctly:
cd /var/www/hugo
sudo -u hugo git clone --depth 1 <your-theme-repo> themes/<your-theme>
sudo -e /var/www/hugo/hugo.toml
# add: theme = "<your-theme>"
sudo rm -rf /var/www/hugo/layouts
sudo hugo-publishKeeping your site in Git is the usual next step. Hold content/ (and layouts/ if you have customised it) in a repository, pull it onto the VM, and let hugo-publish.path publish the result.
Prove the whole chain
The appliance ships the same check the build gate runs. It verifies each of the following:
- The binary is the pinned extended build.
- The site re-renders right now, and the SCSS compiled.
- nginx serves, byte for byte, what was rendered.
- HTTPS presents this VM's own certificate.
- Every asset the page references returns 200 over both HTTP and HTTPS.
- The published
baseURLis a routable address.
sudo hugo-selftestExpected output:
hugo-selftest — sac Hugo appliance ok hugo v0.164.0 extended: hugo v0.164.0-...+extended linux/amd64 ... ok site re-renders and assets/scss/main.scss compiled to CSS (3410 bytes) ok nginx serves the current render byte-for-byte, and it is Hugo-generated ok HTTPS serves the same site with this VM's certificate ok all 8 referenced local assets return 200 over HTTP and HTTPS ok baseURL is routable: http://203.0.113.10/ hugo-selftest: PASS — the appliance builds the site and serves exactly what it built.The asset check catches a failure that a browser test misses. A page can return 200 while every stylesheet and image underneath it returns 404. Requesting only the page cannot see that, but requesting everything the page references can.
Troubleshooting
hugo-publish failed and my page is not live. The design is working. Hugo swaps in the render only once it has succeeded, so the previous site is still up. The error Hugo printed names the file and line. Fix it and run sudo hugo-publish again.
I edited a file and nothing happened. The timer sweeps every minute, so wait 60 seconds or run sudo hugo-publish to publish immediately. If neither works, check that the watchers are running with systemctl is-active hugo-publish.timer hugo-publish.path. Then read sudo journalctl -u hugo-publish.service.
A new page does not appear even after publishing. Hugo skips pages with draft = true in their front matter, and pages with a date in the future. Remove the draft line, check the date, and publish again.
The site is unstyled. The stylesheet is compiled from SCSS at publish time, so an unstyled page means the render did not produce it. Run sudo hugo-selftest. It fails explicitly if the compiled stylesheet is missing or if the binary is not the extended edition.
Links point at the wrong host. baseURL in hugo.toml is what Hugo bakes into the canonical tag, the RSS feed and the sitemap. First boot sets it to this VM's public address. Change it when you point a domain at the site, then publish.
nginx will not start after a reboot. nginx will not start until /var/lib/sac/data/hugo-bootstrap-ready exists. First boot creates that file only after it generates this VM's certificate and renders the site. Check sudo systemctl status hugo-firstboot.service and sudo cat /var/log/sac/sac-firstboot.log.
Security notes
There is no login, and that is intentional. A static site generator is a build tool. Someone who already holds shell access edits the content over SSH, and the generated site is public by design. This appliance ships no admin panel, no login and no upload form, so there is no default credential and nothing authenticated exposed to the internet. If you need an authenticated editing workflow, keep the content in a Git repository and pull it onto the VM.
No key material ships in the image. The TLS certificate and its private key are generated on your VM at first boot. The image contains neither. nginx is gated on a marker that first boot creates only after that has happened, so the appliance cannot serve before it has its own key material. The private key is root:root mode 0600.
The Hugo development server is never run. hugo server binds a live reload listener and watches the filesystem. It is a development tool and is not installed as a service here. nginx serves pre-rendered bytes only.
The site source is not served. nginx's document root is /var/www/hugo/public. Your Markdown, templates, SCSS and hugo.toml sit one level above it and are unreachable over HTTP. Directory listing is off, so a directory without an index.html returns a 404 rather than a file listing. The nginx version is not advertised.
Hugo renders your own content with unsafe = true. Raw HTML in your Markdown is passed through, which is what you want when the author is you. If you ever accept Markdown from untrusted contributors, set unsafe = false in hugo.toml first.
Restrict SSH to your management network with an NSG rule. Leave ports 80 and 443 open only as widely as the site's audience requires.
Keep the OS patched. Unattended security upgrades are enabled. To update Hugo itself, replace /usr/local/bin/hugo with a newer upstream release and run sudo hugo-publish --force.
Support
SecureAnyCloud provides 24/7 support with a guaranteed 24-hour response SLA for all Azure Marketplace images. Contact sales@secureanycloud.com.
Upstream project documentation is at gohugo.io, and the source is at github.com/gohugoio/hugo. Hugo is distributed under the Apache License 2.0 and is redistributed here unmodified. The licence text is retained on the image at /usr/local/share/hugo/LICENSE. SecureAnyCloud is not affiliated with or endorsed by the Hugo project.
Deploy on Azure
Launch Hugo on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com