hledger on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of hledger on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. hledger is a mature open source plain text accounting system. Instead of a database, your entire ledger lives in a single human readable text file that you own, can read without any special software, can diff and can keep in version control. hledger reads that journal and produces real double entry accounting reports: balance sheets, income statements, account registers, budgets and forecasts.
The image installs the pinned hledger 1.52.1 release (recorded in /opt/hledger/VERSION) as fully static binaries, providing three interfaces over the same journal: the hledger command line tool, the hledger-ui terminal interface, and hledger-web, a browser interface run under systemd as the unprivileged hledger system user and bound to loopback only (127.0.0.1:5000). nginx is the sole network facing surface: it terminates TLS on port 443, enforces HTTP basic authentication on every request and reverse proxies to hledger-web. Port 80 redirects to HTTPS.
Secure by default, and it matters more here than for most applications. hledger-web ships with no authentication of any kind — the upstream manual states plainly that there is no access control and that it should be placed behind a suitable web proxy. Worse, its default access level permits anyone who can reach it to add transactions to your journal. This image never exposes it that way. hledger-web is bound to loopback and fronted by nginx basic authentication, and a unique administrator credential is generated on each virtual machine's first boot. Only a one way bcrypt hash of the password is written to the server; the plain password is placed in a root only file for the administrator to read. There are no shared or default credentials, and nothing usable is baked into the image.
The web interface is configured deliberately in append only mode: new transactions can be recorded from the browser, but existing history cannot be altered, deleted, uploaded over or downloaded. Step 11 explains how to change this.
What is included:
-
hledger 1.52.1, hledger-ui 1.52.1 and hledger-web 1.52.1 installed as fully static binaries in
/usr/local/bin, with man pages and bash completion -
hledger-web run under systemd as the unprivileged
hledgersystem user, bound to loopback only (127.0.0.1:5000); it is never network exposed directly -
nginx terminating TLS on :443, enforcing HTTP basic authentication and reverse proxying to hledger-web, with HSTS and sensible security headers
-
Port
:80returns a301redirect to HTTPS for every path except an unauthenticated/healthzendpoint (HTTP 200) for load balancer probes -
A unique admin credential generated per VM on first boot; only the one way bcrypt hash is stored, the plain password lands in a root only file
-
A self signed TLS certificate regenerated per VM on first boot (its Subject Alternative Names include the VM public IP, hostname and
127.0.0.1); replace it with your own CA signed certificate for production -
A documented starter journal at
/var/lib/hledger/main.journal, readable only by the service account and root because it is financial data -
The web interface set to append only (
--allow=add) so recorded history cannot be rewritten from the browser -
A built in self test at
/usr/local/bin/hledger-selftestthat proves the accounting engine, the authentication gate and the web interface end to end -
The GNU GPL v3 licence text and a pointer to the corresponding source in
/opt/hledger -
Ubuntu 24.04 LTS base with the latest security patches applied at build time
-
Azure Linux Agent for seamless cloud integration and SSH key injection
-
24/7 cloudimg support with a guaranteed 24 hour response SLA
Prerequisites
-
An Azure subscription with permission to deploy virtual machines
-
An SSH key pair for administrative access to the VM as the
azureuseraccount -
A Network Security Group allowing inbound TCP
443(HTTPS) and80(redirect) from the networks that should reach the service, and22(SSH) from your management network only -
A recommended size of Standard_B2s or larger
Step 1: Deploy from the Azure Portal
-
Locate the hledger on Ubuntu 24.04 LTS image in the Azure Marketplace and select Create.
-
Choose your subscription, resource group and region.
-
Select a VM size (Standard_B2s or larger) and provide your SSH public key for the
azureuseraccount. -
On the Networking tab, allow inbound
443and80from your users, and restrict22to your management network. -
Review and create. When the VM is running, browse to
https://<your-public-ip>/.
Step 2: Deploy from the Azure CLI
Deploy the image from the command line, restricting SSH to your management network. Replace the placeholder values with your own before running:
az vm create \
--resource-group my-resource-group \
--name hledger \
--image <marketplace-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_rsa.pub \
--public-ip-sku Standard
# Allow the web interface from your users and SSH from your management network only
az vm open-port --resource-group my-resource-group --name hledger --port 443 --priority 1001
az network nsg rule create --resource-group my-resource-group --nsg-name hledgerNSG \
--name allow-ssh --priority 1002 --destination-port-ranges 22 \
--source-address-prefixes <your-mgmt-cidr> --access Allow --protocol Tcp
When the VM is running, browse to https://<public-ip>/.
Step 3: Retrieve your per instance admin credential
Access is protected by HTTP basic authentication. Each VM generates its own admin username and password on first boot, written to a root only file. Read it over SSH:
ssh azureuser@<vm-ip> 'sudo cat /root/hledger-credentials.txt'
The file contains the HLEDGER_USER, HLEDGER_PASS and HLEDGER_URL for this specific VM. Keep it secret. The server only ever stores a one way bcrypt hash of the password, never the plain text.
Step 4: Sign in to the web interface
Browse to https://<vm-ip>/. Because the certificate is self signed per VM, your browser will show a certificate warning the first time; accept it to proceed (or install the per VM certificate, or place a CA signed certificate in front, as covered in Step 12). Your browser then prompts for the basic authentication username and password from Step 3.
You land on the journal view. The sidebar lists your accounts with their current balances, and the main panel lists every transaction in the journal, newest first.

Step 5: Understand the journal file
Everything hledger reports comes from one plain text file:
ssh azureuser@<vm-ip>
sudo -u hledger cat /var/lib/hledger/main.journal
The journal is owned by the hledger service account with mode 0640 inside a 0750 directory, so it is readable only by that account and by root. It is financial data and is deliberately not world readable.
A transaction looks like this. Every transaction must balance to zero, and leaving the last amount blank lets hledger infer it:
2026-01-08 * Client invoice 001
assets:bank:current 1200.00 GBP
income:consulting
Accounts are created simply by naming them, and colons make a hierarchy, so assets:bank:current sits under assets:bank, which sits under assets.
You do not need to pass -f to point hledger at this file. The image wires journal discovery two ways: LEDGER_FILE is preset for interactive shells, and hledger's own default path ($HOME/.hledger.journal) is symlinked to the journal for both the hledger service account and root. That means sudo -u hledger hledger balance and sudo hledger balance both work from scripts, cron jobs and non interactive sessions, not only from a login shell.
Run reports as the hledger account, which is the identity that owns the ledger:
sudo -u hledger hledger balance
Step 6: Record a transaction
Append a transaction to the journal with any editor, or from the shell. This example records a second client invoice:
sudo -u hledger tee -a /var/lib/hledger/main.journal >/dev/null <<'TXN'
2026-01-20 * Client invoice 002
assets:bank:current 850.00 GBP
income:consulting
TXN
hledger also has an interactive prompt driven entry mode, which suggests accounts as you type:
sudo -u hledger hledger add
After any edit, confirm the journal still parses:
sudo -u hledger hledger check
Expected output is silence on success. Because the file is plain text, a typo is a parse error you can see and fix, not a corrupted database.
Step 7: Run balance and register reports
The balance report totals every account; the register report shows the individual postings to an account with a running total. Both read the journal you just edited:
sudo -u hledger hledger balance
sudo -u hledger hledger register income:consulting
Note that the new invoice is reflected immediately: assets:bank:current has risen to 3976.55 GBP and income:consulting to -2050.00 GBP. Income and equity accounts show as negative under standard double entry sign conventions.

Other reports worth knowing:
sudo -u hledger hledger balancesheet # assets and liabilities, with net worth
sudo -u hledger hledger incomestatement # revenues and expenses over the period
sudo -u hledger hledger accounts # every account name in the journal
sudo -u hledger hledger stats # journal size, date span, transaction count
Step 8: Explore an account in the browser
Click any account in the sidebar to open its register. The browser view adds a running balance chart over the period alongside the postings table, which is the quickest way to see how an account moved over time.

The search box accepts hledger's query language, so you can narrow any view. For example inacct:expenses shows only expense postings, date:2026-01 limits to January 2026, and amt:>500 filters by size.

Step 9: Add a transaction from the browser
Select Add a transaction on the journal view. The form takes a date, a description and up to four account and amount pairs; leaving the final amount blank lets hledger infer the balancing posting, exactly as in the file. Selecting Save appends the transaction to the same /var/lib/hledger/main.journal file, so a CLI hledger balance reflects it immediately.

There is no separate database to keep in step: the browser, the terminal and the file are three views of one journal.
Step 10: Verify the deployment
Confirm the services are running and that hledger-web is reachable only through nginx:
systemctl is-active hledger-web nginx hledger-firstboot
ss -tlnp | grep -E ':5000 |:443 |:80 '
hledger-web must appear on 127.0.0.1:5000 and nowhere else. nginx holds :80 and :443.

Step 11: The authentication gate and the access level
Every path except the health probe requires the per VM credential. Confirm it:
curl -sk -o /dev/null -w '%{http_code}\n' https://127.0.0.1/ # 401
curl -sk -o /dev/null -w '%{http_code}\n' https://127.0.0.1/journal # 401
curl -sk -o /dev/null -w '%{http_code}\n' -X POST https://127.0.0.1/add # 401
curl -sk -o /dev/null -w '%{http_code}\n' https://127.0.0.1/healthz # 200

The image runs hledger-web with --allow=add, which permits recording new transactions but never altering or deleting existing history, and never uploading or downloading the journal file.
To change it, drop in a systemd override rather than editing the shipped unit, so your choice survives any future package update. This example switches the browser interface to strictly read only:
sudo install -d /etc/systemd/system/hledger-web.service.d
sudo tee /etc/systemd/system/hledger-web.service.d/10-access-level.conf >/dev/null <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/local/bin/hledger-web --serve --host=127.0.0.1 --port=5000 --allow=view --file=/var/lib/hledger/main.journal
EOF
sudo systemctl daemon-reload
sudo systemctl restart hledger-web
The empty ExecStart= line is required: it clears the shipped command before setting the replacement.
The three useful values for --allow are:
-
--allow=view— strictly read only. The browser becomes a report viewer; transactions can only be added by editing the file or using the CLI. -
--allow=add— the default on this image. Append only: new transactions can be recorded from the browser, existing history cannot be changed. -
--allow=edit— additionally permits editing, uploading and downloading the journal file. Convenient, but a single bad upload replaces your entire ledger. Choose it only if you understand that.
Confirm which level is active at any time with systemctl show -p ExecStart hledger-web | grep -o -- '--allow=[a-z]*'.
Step 12: Replace the certificate for production
The per VM certificate is self signed. For production, point a real domain at the VM and install a CA signed certificate:
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.example.com
certbot edits the nginx server block in place and sets up renewal. The basic authentication gate and the loopback binding are unaffected.
Step 13: Back up your journal
Because the ledger is one plain text file, backup is a file copy, and version control works properly:
# Point in time copy
sudo -u hledger cp /var/lib/hledger/main.journal \
/var/lib/hledger/main.journal.$(date +%F)
# Or track the journal in git, which gives you a full, diffable audit history
cd /var/lib/hledger && sudo -u hledger git init && sudo -u hledger git add main.journal
sudo -u hledger git commit -m "January 2026 accounts"
Copy the file off the VM regularly, for example to Azure Blob Storage, and take VM level snapshots for disaster recovery. A journal committed to git gives you a reviewable change history that no database format can match.
Step 14: Baked version and the built in self test
The image records the exact version it shipped, and includes a self test that proves the accounting engine rather than merely checking that a port is open. It computes a balance from a freshly written transaction, confirms the shipped journal reports correctly through the CLI, checks that unauthenticated requests are refused, and confirms the authenticated web interface serves the same data:
cat /opt/hledger/VERSION
sudo /usr/local/bin/hledger-selftest

Security notes
-
No default login. Access is protected by nginx HTTP basic authentication and a unique admin credential is generated on each VM's first boot. Only a one way bcrypt hash is stored; the plain password lives in the root only
/root/hledger-credentials.txt. -
hledger-web has no authentication of its own. Upstream is explicit that it has no access control and must sit behind a proxy, and its default access level lets anyone who can reach it add transactions. This image binds it to
127.0.0.1:5000and puts nginx in front. Never change the--hostsetting to expose it directly. -
Append only by default. The web interface runs with
--allow=add, so recorded history cannot be rewritten or deleted from the browser and the journal file cannot be uploaded over. Review Step 11 before widening this to--allow=edit. -
Your journal is financial data. It is mode
0640owned by thehledgerservice account inside a0750directory, so it is not world readable. Keep it that way, and remember that anyone with the basic authentication credential can read the whole ledger. -
Restrict access. Allow
443only from the networks that need it, keep22restricted to your management network, and replace the self signed certificate with a CA signed one for production. Basic authentication credentials over plain HTTP would be exposed, which is why:80only redirects. -
Rotate the credential by regenerating the nginx htpasswd entry (
sudo htpasswd -B /etc/nginx/hledger.htpasswd admin) and reloading nginx, or by placing your own identity aware proxy in front.
Support
This image is backed by 24/7 cloudimg support with a guaranteed 24 hour response SLA. Contact support@cloudimg.co.uk for assistance. hledger is open source software distributed under the GNU General Public License version 3 or later and is free; the licence text and a pointer to the corresponding source are included on the image in /opt/hledger. The cloudimg charge covers packaging, hardening, security patching, image maintenance and support.