Applications Azure

LedgerSMB on Ubuntu 24.04 on Azure User Guide

| Product: LedgerSMB on Ubuntu 24.04 LTS on Azure

Overview

LedgerSMB is an open source double entry accounting and ERP system for small and medium businesses. It has a long lineage: it began as a fork of SQL-Ledger in 2006 and has been maintained continuously since, with its own published security advisories and a release cadence that has kept the 1.13 line current through 2026.

It covers the whole commercial cycle in one ledger. On the sales side: quotations, sales orders, invoices, receipts and customer statements. On the purchase side: requests for quotation, purchase orders, vendor invoices and payments. Around those sit parts and inventory, projects and timecards, fixed assets, budgeting, bank reconciliation, and a full general ledger with trial balance, income statement and balance sheet reporting. Customer facing documents are template driven and can be rendered as PDF, HTML, CSV and ODF, and a REST interface is available for integration.

The cloudimg image delivers LedgerSMB 1.13.8 on Ubuntu 24.04, served over HTTPS, with a company database and chart of accounts already created and every secret generated on the first boot of your VM. Backed by 24/7 cloudimg support.

What is included:

  • LedgerSMB 1.13.8 at /opt/ledgersmb, installed from the vendor's own release asset and verified by SHA-256
  • starman, upstream's own recommended PSGI server, bound to 127.0.0.1:5762 only
  • nginx serving HTTPS on port 443 with a certificate generated for your VM; port 80 only redirects to HTTPS
  • PostgreSQL 16 on the loopback address only, holding the company ledger
  • A company database named ledgersmb with a general chart of accounts already loaded, so you sign in and start work
  • No default login: the administrator password, the database passwords and the TLS key are all generated on your VM's first boot and written to a root-only file
  • The setup.pl database administration console gated by two independent checks — an HTTP authentication challenge at the web server, and PostgreSQL authentication in the application
  • postgresql@16-main, ledgersmb and nginx systemd services, enabled and active

LedgerSMB is a trademark of its respective owner. All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them. This image packages the unmodified open source software, which is distributed under the GNU General Public License version 2.

The LedgerSMB sign-in page served over HTTPS

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU, 4 GiB RAM) is the recommended size and is what this image is tested at. LedgerSMB runs under starman with three preforked workers, each holding a resident copy of the application, so memory rather than CPU is the constraint; choose a larger size for many concurrent users or heavy reporting. Network security group inbound rules: 22/tcp from your management network, 443/tcp for the web interface, and optionally 80/tcp, which only redirects to HTTPS.

Step 1: Deploy from the Azure Marketplace

Find LedgerSMB on Ubuntu 24.04 LTS in the Azure Marketplace, choose Create, and complete the wizard: subscription and resource group, region, VM size Standard_B2s, an administrator username of azureuser with your SSH public key, and inbound ports 22 and 443. Leave the OS disk as the default managed disk. The VM is ready to use as soon as its first boot has finished provisioning, which takes under a minute after the VM reports Running.

Step 2: Deploy from the Azure CLI

The same deployment from the command line. Run these on your own workstation, not on the VM. Replace the resource group, region and management CIDR to suit.

az group create --name ledgersmb-rg --location eastus

az network vnet create --resource-group ledgersmb-rg --name ledgersmb-vnet \
  --address-prefix 10.10.0.0/16 --subnet-name app --subnet-prefix 10.10.1.0/24

az network nsg create --resource-group ledgersmb-rg --name ledgersmb-nsg

az network nsg rule create --resource-group ledgersmb-rg --nsg-name ledgersmb-nsg \
  --name allow-ssh --priority 1000 --destination-port-ranges 22 \
  --source-address-prefixes <your-mgmt-cidr> --access Allow --protocol Tcp

az network nsg rule create --resource-group ledgersmb-rg --nsg-name ledgersmb-nsg \
  --name allow-https --priority 1010 --destination-port-ranges 443 \
  --source-address-prefixes Internet --access Allow --protocol Tcp

az vm create --resource-group ledgersmb-rg --name ledgersmb-01 \
  --image cloudimg:ledgersmb-ubuntu-24-04:default:latest \
  --size Standard_B2s --admin-username azureuser --generate-ssh-keys \
  --vnet-name ledgersmb-vnet --subnet app --nsg ledgersmb-nsg \
  --public-ip-sku Standard

Step 3: Confirm the stack is running

SSH to the VM as azureuser and check the three services. postgresql@16-main is the unit that actually holds the database: the postgresql wrapper unit can report active while the real cluster unit has failed, so this is the one worth checking.

systemctl is-active postgresql@16-main ledgersmb nginx
cat /etc/ledgersmb/version
sudo -u postgres psql -tAc 'SHOW server_version'

Expected output:

active
active
active
1.13.8
16.15 (Ubuntu 16.15-0ubuntu0.24.04.1)

The LedgerSMB service stack running, with the shipped release and PostgreSQL version, and the listening sockets showing starman and PostgreSQL on loopback only

Step 4: Check what is listening

Only SSH and the two web ports are reachable from outside the VM. The application server and the database are both bound to the loopback address, so neither can be reached from the network at all.

sudo /usr/local/sbin/ledgersmb-port-check.sh

Expected output:

off-box listeners: [22 80 443]
expected:          [22 80 443]
LEDGERSMB_PORTS_OK

To see the detail behind that, including the loopback-only bindings:

ss -lnt | grep -E '5762|5432|:443|:80 |:22'

The positive off-box port assertion: the reachable set is exactly SSH, HTTP and HTTPS

Step 5: Retrieve your credentials

Nothing secret is baked into the image. On the first boot of your VM, LedgerSMB generates its own TLS key pair, its own database role password and its own administrator password, and writes them to a file that only root can read.

sudo ls -l /root/ledgersmb-credentials.txt
sudo grep -E '^LEDGERSMB_(URL|COMPANY|ADMIN_USER|SETUP_USER)=' /root/ledgersmb-credentials.txt

Expected output:

-rw------- 1 root root 767 Sep 20 15:52 /root/ledgersmb-credentials.txt
LEDGERSMB_URL=https://20.0.0.10/
LEDGERSMB_COMPANY=ledgersmb
LEDGERSMB_ADMIN_USER=admin
LEDGERSMB_SETUP_USER=lsmb_setup

The address, the timestamp and the exact byte count will differ on your instance: the passwords are generated per VM, so the file's length is not fixed.

To read the two passwords themselves:

sudo awk -F= '/^LEDGERSMB_ADMIN_PASSWORD=/{print $2}' /root/ledgersmb-credentials.txt
sudo awk -F= '/^LEDGERSMB_SETUP_PASSWORD=/{print $2}' /root/ledgersmb-credentials.txt

Keep this file, or copy the values into your password manager. These values are unique to this VM and are not recorded anywhere else — neither cloudimg nor anyone else can recover them for you.

The per-VM credentials file, generated on first boot and readable only by root

Step 6: Sign in to the ledger

Browse to https://<the VM's public address>/ and you will be sent to the sign-in page. Enter the company ledgersmb, the username admin, and the administrator password from the previous step.

Because the certificate generated on first boot is self signed, your browser will warn you the first time. That is expected; Step 12 replaces it with a certificate for your own domain.

You can confirm the credential works end to end from the command line before you open a browser. This performs a real sign-in through the front door and then requires the resulting session to land on an authenticated page — a status code alone is not proof, because LedgerSMB issues a session cookie to anonymous requests too.

sudo /usr/local/sbin/ledgersmb-verify-login.sh \
  "$(sudo awk -F= '/^LEDGERSMB_ADMIN_USER=/{print $2}' /root/ledgersmb-credentials.txt)" \
  "$(sudo awk -F= '/^LEDGERSMB_ADMIN_PASSWORD=/{print $2}' /root/ledgersmb-credentials.txt)" \
  "$(sudo awk -F= '/^LEDGERSMB_COMPANY=/{print $2}' /root/ledgersmb-credentials.txt)"

Expected output:

LEDGERSMB_LOGIN_OK (authenticate=200 landing=200)

The LedgerSMB application after signing in

Step 7: The chart of accounts

The company database ships with a general chart of accounts already loaded, so the ledger is usable immediately rather than presenting you with an empty database and a setup wizard. You can see it under General Ledger → Chart of Accounts in the web interface, or count it directly:

sudo -u postgres psql -d ledgersmb -tAc \
  "SELECT count(*) || ' accounts, ' || count(*) FILTER (WHERE category='A') || ' asset' FROM account"

Expected output:

102 accounts, 20 asset

The loaded chart of accounts rendered in the web interface

If you would rather start from a different national chart, LedgerSMB ships charts for 29 countries. List them, then load one through the setup.pl console described in the next step when you create a new company:

sudo ls /opt/ledgersmb/locale/coa/
sudo ls /opt/ledgersmb/locale/coa/us/

Step 8: The database administration console, and why it is gated twice

LedgerSMB has a second front door at /setup.pl. It is not part of the accounting application: it creates, upgrades, copies, backs up and destroys company databases, and it authenticates as a PostgreSQL superuser. On this image it is therefore protected by two checks that fail independently.

The first is at the web server. An unauthenticated request never reaches the application at all:

curl -sk -o /dev/null -w 'unauthenticated /setup.pl -> %{http_code}\n' https://127.0.0.1/setup.pl

Expected output:

unauthenticated /setup.pl -> 401

That check covers every spelling of the path, not just the obvious one, and it keeps working even if the database is stopped, because it consults no database:

for p in /setup.pl /Setup.pl //setup.pl /./setup.pl /x/../setup.pl /%73etup.pl; do
  printf '%-20s -> %s\n' "$p" "$(curl -sk -o /dev/null -w '%{http_code}' https://127.0.0.1$p)"
done

Expected output:

/setup.pl            -> 401
/Setup.pl            -> 401
//setup.pl           -> 401
/./setup.pl          -> 401
/x/../setup.pl       -> 401
/%73etup.pl          -> 401

The second check is in the application itself, which authenticates the same credential against PostgreSQL. Published, blank and weak credentials are refused:

for c in admin:admin postgres:postgres lsmb_setup: lsmb_setup:password root:root; do
  printf '%-22s -> %s\n' "$c" "$(curl -sk -o /dev/null -w '%{http_code}' -u "$c" https://127.0.0.1/setup.pl)"
done

Expected output:

admin:admin            -> 401
postgres:postgres      -> 401
lsmb_setup:            -> 401
lsmb_setup:password    -> 401
root:root              -> 401

Your own per-VM credential passes both:

curl -sk -o /dev/null -w 'per-VM setup credential -> %{http_code}\n' \
  -u "$(sudo awk -F= '/^LEDGERSMB_SETUP_USER=/{print $2}' /root/ledgersmb-credentials.txt):$(sudo awk -F= '/^LEDGERSMB_SETUP_PASSWORD=/{print $2}' /root/ledgersmb-credentials.txt)" \
  https://127.0.0.1/setup.pl

Expected output:

per-VM setup credential -> 200

To use the console in a browser, go to https://<the VM's public address>/setup.pl. It takes two steps, because the two checks really are separate systems:

  1. Your browser shows an HTTP authentication prompt. This is nginx. Enter lsmb_setup and the setup password.
  2. LedgerSMB then presents its own form, headed Database administrator credentials. Enter the same username and password, and ledgersmb as the database. This one is PostgreSQL authenticating you.

Passing the first does not get you past the second. One first-boot credential opens both, so there is only one password to keep, but an attacker has to defeat two independent mechanisms — and the first of them never reaches the application or the database at all.

The database administration console refusing an unauthenticated request

Once past both checks, the console reports the database it found and offers the administrative actions — creating and copying companies, adding users, loading templates, taking backups and running diagnostics.

The LedgerSMB Database Management Console after passing both checks

If you later want a second person to reach setup.pl, add them to the web server's credential file and create a matching PostgreSQL role — both checks must pass, so both must know about them.

Step 9: Add your own users

Every LedgerSMB user is also a PostgreSQL role, which is why users are created from inside the application rather than in a configuration file. Sign in as admin and go to System → Users → Add User. Give the account a username, a password and the permission sets it needs, and save.

You can confirm the account was created, and see which roles it holds:

sudo -u postgres psql -d ledgersmb -tAc \
  "SELECT username FROM users ORDER BY username"

After a fresh deployment, before you add anyone, this lists only the administrator created on first boot:

admin

Step 10: Create a second company

A LedgerSMB "company" is a separate database with its own ledger, which is how you keep several legal entities apart on one instance. Create one from the setup.pl console: sign in with the setup credential, enter a new database name, choose a chart of accounts, and create the initial user for that company.

The console does the same work as the command line tool shipped with the application. These are the commands it offers:

sudo ls /opt/ledgersmb/lib/LedgerSMB/Admin/Command/ | sed 's/\.pm$//'

Expected output:

backup
copy
create
destroy
rename
restore
setup
template
upgrade
user

Run the tool as sudo PERL5LIB=/opt/ledgersmb-deps/lib/perl5 perl -I /opt/ledgersmb/lib -I /opt/ledgersmb/old/lib /opt/ledgersmb/bin/ledgersmb-admin --help for its full usage text. Note that it exits with a non-zero status after printing help or version information, which is upstream behaviour rather than an error.

Step 11: Change the administrator password

Change it from the web interface under Preferences → Password, which is the supported path and keeps the application's own records consistent.

If you need to reset it from the command line — for instance if the password has been lost — use the provisioning helper that first boot itself uses. It reads both passwords from standard input so that neither ever appears in ps output, in /proc or in the system journal. Replace <new-password> with the password you want:

printf '%s\n%s\n' \
  "$(sudo awk -F= '/^LEDGERSMB_SETUP_PASSWORD=/{print $2}' /root/ledgersmb-credentials.txt)" \
  '<new-password>' \
  | sudo PERL5LIB=/opt/ledgersmb-deps/lib/perl5 perl \
      -I /opt/ledgersmb/lib -I /opt/ledgersmb/old/lib \
      /usr/local/sbin/ledgersmb-provision-admin.pl ledgersmb lsmb_setup admin

Remember to update /root/ledgersmb-credentials.txt afterwards so the file and the account continue to agree.

Step 12: Use your own domain and certificate

The certificate generated on first boot is self signed. Point a DNS A record at the VM's public address, then obtain a real certificate. Replace <your-domain> with your hostname:

sudo apt-get install -y certbot
sudo certbot certonly --standalone -d <your-domain> --agree-tos -m admin@<your-domain> --non-interactive
sudo sed -i "s#/etc/ledgersmb/tls/ledgersmb.crt#/etc/letsencrypt/live/<your-domain>/fullchain.pem#" /etc/nginx/sites-available/ledgersmb
sudo sed -i "s#/etc/ledgersmb/tls/ledgersmb.key#/etc/letsencrypt/live/<your-domain>/privkey.pem#" /etc/nginx/sites-available/ledgersmb
sudo nginx -t && sudo systemctl reload nginx

nginx answers for any hostname, so no other change is needed. Renewal is handled by the certbot timer that the package installs.

Step 13: Verify the security posture

A short self test you can run at any time. It confirms that the per-VM credential works, that published defaults are refused, that the administration console is gated, and that nothing but SSH and the web ports is reachable from off the VM.

echo "--- off-box listeners ---"
sudo /usr/local/sbin/ledgersmb-port-check.sh
echo "--- unauthenticated setup.pl ---"
curl -sk -o /dev/null -w 'setup.pl -> %{http_code}\n' https://127.0.0.1/setup.pl
echo "--- published default refused ---"
curl -sk -o /dev/null -w 'admin:admin -> %{http_code}\n' -u admin:admin https://127.0.0.1/setup.pl
echo "--- administrator password does not expire ---"
sudo -u postgres psql -tAc "SELECT rolname, COALESCE(rolvaliduntil::text,'infinity') FROM pg_roles WHERE rolname IN ('admin','lsmb_setup')"
echo "--- credentials file permissions ---"
sudo stat -c '%n %U:%G %a' /root/ledgersmb-credentials.txt /etc/ledgersmb/tls/ledgersmb.key

Expected output:

--- off-box listeners ---
off-box listeners: [22 80 443]
expected:          [22 80 443]
LEDGERSMB_PORTS_OK
--- unauthenticated setup.pl ---
setup.pl -> 401
--- published default refused ---
admin:admin -> 401
--- administrator password does not expire ---
admin|infinity
lsmb_setup|infinity
--- credentials file permissions ---
/root/ledgersmb-credentials.txt root:root 600
/etc/ledgersmb/tls/ledgersmb.key root:root 600

The administration console gated: an unauthenticated request is refused with 401 and a real authentication challenge, while the per-VM credential is accepted

Step 14: Back up and restore

Your ledger lives in the ledgersmb database. Back it up with the standard PostgreSQL tooling, and keep the backups off the VM.

sudo -u postgres pg_dump --format=custom --file=/var/tmp/ledgersmb-$(date +%F).dump ledgersmb
ls -lh /var/tmp/ledgersmb-*.dump

To restore into a fresh company database, replace <backup-dir> with the directory holding your dump:

sudo -u postgres createdb ledgersmb_restored
sudo -u postgres pg_restore --dbname=ledgersmb_restored <backup-dir>/ledgersmb-2026-09-20.dump

Also keep a copy of /root/ledgersmb-credentials.txt: the database dump does not contain the role passwords, and without them the restored company cannot be signed into.

Step 15: Keep the instance patched

The image ships with unattended-upgrades enabled, so security updates install automatically. To apply everything immediately:

sudo apt-get update
sudo apt-get upgrade -y
sudo systemctl restart ledgersmb nginx

LedgerSMB itself is installed from the upstream release tarball rather than from a distribution package, so application upgrades are a deliberate step: take a backup, unpack the new release alongside, and run the schema upgrade from setup.pl, which detects the version difference and applies the migration.

Step 16: Grow the disk as your ledger grows

The ledger, including document attachments, lives in PostgreSQL on the OS disk. To expand it, resize the disk in the Azure portal or CLI while the VM is deallocated, then grow the filesystem:

lsblk
sudo growpart /dev/sda 1
sudo resize2fs /dev/sda1
df -h /

Troubleshooting

The web interface does not answer at all. The web tier deliberately refuses to serve until first boot has finished generating this VM's secrets. Check that it completed:

systemctl is-active ledgersmb-firstboot.service
ls -l /var/lib/cloudimg/ledgersmb-firstboot.done
sudo journalctl -u ledgersmb-firstboot.service --no-pager -n 40

A browser certificate warning. Expected until Step 12; the first-boot certificate is self signed.

setup.pl prompts for a password and will not accept the administrator password. They are different credentials. setup.pl wants LEDGERSMB_SETUP_USER and LEDGERSMB_SETUP_PASSWORD; the ledger itself wants LEDGERSMB_ADMIN_USER and LEDGERSMB_ADMIN_PASSWORD.

Sign-in is refused and you are certain the password is right. Confirm the account has not been given an expiry, which is the one thing that would let a correct password stop working:

sudo -u postgres psql -tAc "SELECT rolname, COALESCE(rolvaliduntil::text,'infinity') FROM pg_roles WHERE rolname='admin'"

This must print infinity. If it prints a date, the account was re-created outside the helper in Step 11; re-run that helper to restore it.

The application is running but slow. starman preforks three workers. On a larger VM size raise the worker count in /etc/systemd/system/ledgersmb.service, then sudo systemctl daemon-reload && sudo systemctl restart ledgersmb.

Checking the application's own log. LedgerSMB logs to the journal under its unit:

sudo journalctl -u ledgersmb --no-pager -n 40

Support

cloudimg provides 24/7 technical support for this image by email at support@cloudimg.co.uk and by live chat, with a one hour average response time for critical issues. We help with deployment, chart of accounts selection, company database creation and schema upgrades, document and invoice template customisation, the REST interface, user and role administration, backup and restore, PostgreSQL administration, and troubleshooting.