Algernon on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of Algernon on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Algernon is a small, self contained web and application server written in Go and published under the BSD 3 Clause licence.
The idea behind Algernon is that one static binary should be enough to serve a whole dynamic site. It serves ordinary static files, and it also renders on the fly: Markdown becomes a themed HTML page, Lua files execute server side and write their own response, and Amber, Pongo2, GCSS, TypeScript and JSX are all handled the same way. HTTP/2 and TLS are built in. So is a datastore, so the Lua page you write can keep state without you standing up a database.
That means there is no PHP FPM pool to supervise, no application server to deploy your code into, no reverse proxy in front, and no database daemon behind. There is one process, one configuration surface, and your content directory.
The image ships the official upstream Algernon 1.17.11 release binary, fetched from the project's own GitHub release and pinned by SHA-256. The exact artifact, its digest and its provenance are recorded on the image at /opt/algernon/VERSION. Algernon runs under systemd as algernon.service under an unprivileged algernon account, holding ports 80 and 443 through a single Linux capability rather than by running as root.
A web server that is actually serving something the moment it boots. A web server pointed at an empty directory shows a blank index and reads as broken. So the image ships a small default site, written by cloudimg, that demonstrates the two things Algernon exists for. The landing page at / is index.lua, executed on every request. /welcome.md is a plain Markdown file rendered to HTML when you ask for it. /status.lua computes its response in a loop and echoes back a query parameter, so you can see per request execution rather than take it on trust. All of it is yours to delete once your own content is in place.
What is included:
- Algernon 1.17.11 (official upstream release binary, SHA-256 pinned, redistributed unmodified)
- A cloudimg authored default site demonstrating Markdown and Lua rendering
- A per instance TLS certificate generated at first boot, so no two VMs share key material
- The embedded Bolt datastore, so the Lua storage API works with no extra daemon
algernon-selftest, a one command proof that the server is really rendering- Ubuntu 24.04 LTS, fully patched at build time, with unattended security updates left enabled
- 24/7 cloudimg support
Key facts:
| Item | Value |
|---|---|
| Platform | Ubuntu 24.04 LTS (Azure Gen2) |
| Default SSH user | azureuser |
| Service | algernon.service |
| Runs as | algernon (unprivileged) |
| Document root | /srv/algernon |
| HTTP port | 80 |
| HTTPS port | 443 (HTTP/2) |
| Configuration | systemd unit flags, /etc/systemd/system/algernon.service |
| Datastore | /var/lib/algernon/algernon.db (embedded Bolt) |
Prerequisites
Before deploying, ensure you have:
- An active Azure subscription
- An SSH key pair, or the Azure Portal ability to generate one
- Permission to create virtual machines, virtual networks and network security groups
- A network security group that allows inbound TCP 22 from your management network, and inbound TCP 80 and 443 from wherever your site should be reachable
Recommended VM size: Standard_B2s (2 vCPU, 4 GiB). Algernon itself is very light. Size up only for your own traffic and content.
Step 1: Deploy from the Azure Portal
- In the Azure Portal, choose Create a resource and search the Marketplace for Algernon on Ubuntu 24.04 LTS by cloudimg.
- Select the offer and choose Create.
- On the Basics tab pick your subscription, resource group and region, set the VM name, and choose size
Standard_B2s. - Leave the authentication type as SSH public key and the username as
azureuser. - Under Inbound port rules allow SSH (22), HTTP (80) and HTTPS (443).
- Review and create. The VM is ready in about a minute, and first boot generates this VM's TLS certificate.
Step 2: Deploy from the Azure CLI
az group create --name algernon-rg --location eastus
az vm create \
--resource-group algernon-rg \
--name algernon-vm \
--image cloudimg:algernon:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
# the public address of the new VM
az vm show --resource-group algernon-rg --name algernon-vm \
--show-details --query publicIps --output tsv
Then open the ports your site needs:
az network nsg rule create --resource-group algernon-rg \
--nsg-name algernon-vmNSG --name allow-http \
--priority 1010 --destination-port-ranges 80 443 \
--protocol Tcp --access Allow --direction Inbound
Step 3: Connect and confirm Algernon is running
ssh azureuser@<public-ip>
Confirm the service came up and check which upstream artifact this image shipped:
algernon --version
systemctl is-active algernon.service
cat /opt/algernon/VERSION
Expected output:
Algernon 1.17.11
active
product=Algernon
version=1.17.11
upstream=https://github.com/xyproto/algernon
release=https://github.com/xyproto/algernon/releases/download/v1.17.11/algernon-1.17.11-linux_x86_64_static.tar.xz
tarball_sha256=73075b76ccbc5bf9d383e95ca9987cd66a1bf46f37dfeaa00ec0ad5714871abf
binary_sha256=cd81e9884f8ac9797e32129b7f903421292578eb93ef19dc71007c7efa935a3e
licence=BSD-3-Clause (verbatim text at /opt/algernon/LICENSE)
The whole listening surface of the appliance is three ports, and you can see exactly who owns them:
sudo ss -lntp | grep -E ':(22|80|443)\s'
Ports 80 and 443 belong to algernon. There is no monitor port, no admin console, no debug endpoint and no database listener, because the datastore is an embedded file rather than a separate daemon.

Step 4: See the rendering for yourself
This is what Algernon is for, so it is worth proving rather than assuming. Open http://<ALGERNON_HOST>/ in a browser and you will land on index.lua, executed on the server for that request. Reload it and the generated timestamp moves.
The shipped selftest checks the whole thing in one command:
sudo /usr/local/sbin/algernon-selftest
It exits non zero and says why if anything is wrong. On a healthy VM it reports that Markdown rendered into real HTML elements, that Lua executed per request, that the same site is served over TLS with HTTP/2, and that no REPL, debug or scripting surface is exposed.
Markdown is rendered, not served verbatim. /srv/algernon/welcome.md is an ordinary Markdown file. Ask for it over HTTP and you get HTML:
grep -m1 '^## ' /srv/algernon/welcome.md
curl -s http://127.0.0.1/welcome.md | grep -oE '<h2[^>]*>[^<]*</h2>' | head -1
curl -s http://127.0.0.1/welcome.md | grep -oE '<td>[^<]*</td>' | head -2
The heading arrives as an <h2> element and the pipe table arrives as <td> cells. Neither tag exists anywhere in the source file.
Lua executes on every request. /srv/algernon/status.lua sums the numbers 1 to 100 in a loop and multiplies any ?n= parameter by six, so the answers cannot be sitting in a file waiting to be served:
curl -s http://127.0.0.1/status.lua
curl -s 'http://127.0.0.1/status.lua?n=7' | grep '^echo:'
curl -s 'http://127.0.0.1/status.lua?n=9' | grep '^echo:'
Expected output:
CLOUDIMG_ALGERNON_LUA_OK:5050
echo:0
path:/status.lua
method:GET
tick:12.345678
server:Algernon on Ubuntu 24.04 LTS by cloudimg
echo:42
echo:54
The tick value changes on every request even for an identical URL, which is how you can tell the response is being computed rather than replayed from a cache.

Step 5: TLS and HTTP/2 are already on
Algernon terminates TLS itself. The same site is served on port 443 with HTTP/2 negotiated by ALPN, using a certificate generated on this VM at first boot:
curl -sk https://127.0.0.1/status.lua | head -1
curl -sk --http2 -o /dev/null -w 'negotiated HTTP/%{http_version}\n' https://127.0.0.1/
echo | openssl s_client -connect 127.0.0.1:443 2>/dev/null | openssl x509 -noout -subject -dates
The certificate is self signed, so a browser will warn until you install your own. It is unique to this VM: no certificate and no private key ship in the image, so no two customers, and no two of your own VMs, can ever share TLS key material. The private key lives at /etc/algernon/tls/key.pem as 0640 root:algernon.
Every response also carries the standard protective headers plus a content security policy:
curl -sI http://127.0.0.1/ | grep -iE '^(server|x-content-type-options|x-frame-options|x-xss-protection|content-security-policy):'

Step 6: Read this VM's endpoint summary
First boot writes a summary of this VM's own endpoints and paths, readable only by root:
sudo cat /root/algernon-endpoints.txt
It records the public URL, the document root, the datastore path, the log paths, the certificate fingerprint and the selftest command. Algernon has no login and no password, so there is no credential in it: the file exists so you never have to guess where anything lives.
Step 7: Publish your own content
The document root is /srv/algernon. Drop files in as root and they are served immediately. No build step, no restart:
sudo tee /srv/algernon/example-page.md >/dev/null <<'PAGE'
# My first page
This file was written as plain Markdown and is being rendered on request.
- Markdown becomes a themed HTML page
- A `.lua` file runs server side and writes its own response
- Anything else is served as an ordinary static file
PAGE
curl -s http://127.0.0.1/example-page.md | grep -oE '<h1[^>]*>[^<]*</h1>'
sudo rm -f /srv/algernon/example-page.md
The file extension decides the treatment:
| Extension | Treatment |
|---|---|
.md, .markdown |
rendered as Markdown into a themed HTML page |
.lua, .tl |
executed server side, writes its own response |
.amber, .po2, .pongo2, .tmpl |
rendered as a template |
.gcss, .scss |
rendered to CSS |
.jsx, .tsx, .ts |
transpiled for the browser |
| anything else | served as a static file |
Index files are resolved in this order, so an index.lua wins over an index.html, which wins over an index.md.
The three files that make up the shipped default site are index.lua, welcome.md and status.lua. Delete them once your own site is in place:
ls -la /srv/algernon/
Why the document root is owned by root and not by the service account. Algernon executes any .lua file it finds in the document root, and automatically executes a serverconf.lua there as server configuration if one exists. If the account Algernon runs as could also write into that directory, then any bug that let an attacker write a file would immediately become remote code execution. So the tree is root:root and the systemd unit additionally mounts the whole filesystem read only for the service except the datastore and log directories. Use sudo to publish, and do not chown the tree to the service account.
Step 8: Install your own TLS certificate
Replace the self signed pair with a certificate for your own domain, keeping the same paths and ownership:
sudo install -o root -g algernon -m 0644 \
/etc/letsencrypt/live/<your-domain>/fullchain.pem /etc/algernon/tls/cert.pem
sudo install -o root -g algernon -m 0640 \
/etc/letsencrypt/live/<your-domain>/privkey.pem /etc/algernon/tls/key.pem
sudo systemctl restart algernon
Algernon can also obtain and renew certificates itself using Let's Encrypt, which suits a VM that already has a public DNS name pointed at it. Add --letsencrypt --domain to the ExecStart line in the unit and drop the --cert and --key flags. Test against the staging CA first with --testcert so you do not spend your rate limit on a misconfiguration.
Step 9: Tune the server
All configuration is on the ExecStart line of the systemd unit, so there is one place to look:
systemctl cat algernon.service | grep -A20 ExecStart
The flags this image sets, and why:
| Flag | Why |
|---|---|
--dir=/srv/algernon |
the document root |
--http-addr=:80 / --https-addr=:443 |
serve both, no forced redirect |
--cert / --key |
the per instance certificate generated at first boot |
--boltdb=/var/lib/algernon/algernon.db |
embedded datastore, so no database daemon and no extra listener |
--cache=prod |
cache static files and rendered Markdown, never cache Lua |
--limit=200 |
requests per second per client |
--noninteractive |
no Lua REPL, and debug mode forced off |
--stricter |
adds the content security policy |
--hide-dotfiles |
dotfiles are neither listed nor served |
--servername=Algernon |
identify the product without publishing the exact build |
--accesslog / --log |
request log and server log under /var/log/algernon |
To change one, edit the unit and reload:
$ sudo systemctl edit --full algernon.service
$ sudo systemctl daemon-reload
$ sudo systemctl restart algernon
Two flags are worth knowing about while you are authoring content. --cache=off makes every file re read from disk on each request, which is what you want while editing. --autorefresh injects a small script that reloads the browser when a file changes. Both are development conveniences: leave them off in production, and note that --autorefresh opens an additional event stream port.
Flags this image deliberately does not set. --lua starts an interactive Lua prompt instead of serving. --debug renders failing Lua source, including file paths, straight into the HTTP response. --dev turns on debug mode and raises the rate limit. --nolimit removes rate limiting entirely. None of them belong on an internet facing VM, and the selftest checks that none of them is present on the running process.
Step 10: Watch what the server is doing
sudo tail -5 /var/log/algernon/access.log
sudo tail -5 /var/log/algernon/server.log
sudo journalctl -u algernon.service -n 20 --no-pager
The access log is Combined Log Format, so anything that reads Apache or nginx logs will read it. The server log is JSON, one object per line.
One thing to know about this server: it does not always exit when something goes wrong. If it cannot read its TLS certificate, for example, it logs a single error, drops the HTTPS listener, and carries on serving plain HTTP. systemd will report active (running) and nothing will restart. So when something looks wrong, read server.log rather than trusting the unit state:
sudo grep -c '"level":"error"' /var/log/algernon/server.log || echo 0

Server components
| Component | Version | Purpose |
|---|---|---|
| Algernon | 1.17.11 | web and application server, Markdown and Lua rendering, TLS, HTTP/2 |
| Ubuntu | 24.04 LTS | base operating system |
| systemd | 255 | service supervision and sandboxing |
| OpenSSL | 3.0.x | per instance certificate generation at first boot |
Filesystem layout
| Path | Purpose |
|---|---|
/usr/local/bin/algernon |
the server binary (single static executable) |
/srv/algernon |
document root, root:root 0755 |
/etc/algernon/tls |
per instance certificate and private key |
/var/lib/algernon |
embedded Bolt datastore |
/var/log/algernon |
access log and server log |
/opt/algernon/VERSION |
upstream artifact provenance and digests |
/opt/algernon/LICENSE |
the verbatim BSD 3 Clause licence text |
/usr/local/sbin/algernon-selftest |
the rendering proof |
/root/algernon-endpoints.txt |
this VM's endpoint summary, root only |
Managing the service
sudo systemctl status algernon --no-pager
sudo systemctl restart algernon
Stop it with sudo systemctl stop algernon when you need to. The unit is enabled, so the
server returns automatically after a reboot.
Scripts and log files
| Path | Purpose |
|---|---|
/usr/local/sbin/algernon-firstboot.sh |
generates this VM's TLS certificate on first boot |
/usr/local/sbin/algernon-selftest |
proves Markdown rendering and Lua execution |
/var/log/cloudimg-firstboot.log |
first boot log |
/var/log/algernon/access.log |
requests, Combined Log Format |
/var/log/algernon/server.log |
server log, JSON per line |
/var/lib/cloudimg/algernon-firstboot.done |
first boot sentinel |
On startup
On the very first boot only, algernon-firstboot.service runs before the server starts. It resolves this VM's public address, generates the per instance TLS certificate, writes the endpoint summary, releases the bootstrap gate and starts algernon.service, then proves the server is rendering before declaring itself done.
algernon.service carries ConditionPathExists on a bootstrap marker that only first boot creates. That is deliberate: it means the server physically cannot start before this VM's own key material exists, and because it is a condition rather than an ordering dependency it cannot create the boot job cycle that would hang the VM. Subsequent reboots skip first boot entirely and start the server directly.
Troubleshooting
The site does not answer on port 80 or 443.
systemctl is-active algernon.service
sudo journalctl -u algernon.service -n 40 --no-pager
sudo grep '"level":"error"' /var/log/algernon/server.log | tail || true
sudo grep -q '"level":"error"' /var/log/algernon/server.log || echo "no errors logged"
Check the network security group allows inbound 80 and 443 from where you are testing.
HTTPS is refused but HTTP works. Almost always the certificate or key is unreadable by the service account. The server logs one error and keeps serving plain HTTP:
sudo ls -la /etc/algernon/tls/
sudo grep 'permission denied' /var/log/algernon/server.log || echo "no certificate permission errors logged"
Both /etc/algernon and /etc/algernon/tls must be group algernon, and key.pem must be 0640 root:algernon.
First boot did not complete.
systemctl status algernon-firstboot.service --no-pager
sudo cat /var/log/cloudimg-firstboot.log
A Lua page returns an error but does not say why. That is intentional: debug mode is off, so failing source is never rendered into the response where a visitor could read it. The reason is in the server log:
sudo tail -20 /var/log/algernon/server.log
A page shows stale content after editing. Static files and rendered Markdown are cached (--cache=prod). Restart the service, or switch to --cache=off while authoring.
Requests are being rejected under load. The default ceiling is 200 requests per second per client (--limit). Raise it on the ExecStart line if your traffic pattern needs more.
Security notes
- No login and no default credential. Algernon has no admin console and no seeded account. Its optional user system creates nothing until your own Lua asks it to. Nothing in this image does.
- No key material ships in the image. The TLS certificate and private key are generated on your VM at first boot. The datastore is created on your VM too, so no record from the build can ride along.
- The document root is not writable by the service account, because Algernon executes
.luafiles found there and auto executesserverconf.lua. Publish withsudo. The systemd unit also appliesProtectSystem=strict,NoNewPrivilegesand a capability bounding set of justCAP_NET_BIND_SERVICE. - No REPL and no debug surface. The server runs with
--noninteractiveand reads nothing from standard input. - Restrict SSH to your management network, and expose 80 and 443 only as widely as your site needs.
- Replace the self signed certificate before serving real traffic.
- If you enable the user system, set your own login cookie secret with
SetCookieSecret()in aserverconf.luaplaced in the document root asroot, so sessions survive a restart under a secret you control. - Unattended security updates remain enabled, as on every cloudimg image.
Support
cloudimg provides 24/7 support for this image. Contact support@cloudimg.co.uk.
Algernon itself is developed at github.com/xyproto/algernon and published under the BSD 3 Clause licence. cloudimg is not affiliated with or endorsed by the Algernon project.