Formance Ledger on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and use of Formance Ledger on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Formance Ledger is an open-source programmable double-entry financial core ledger: an HTTP API that records money movements between accounts as immutable, atomic, multi-posting transactions, enforces double-entry accounting invariants, and is programmable in Numscript, a small domain-specific language for expressing balanced postings.
This image ships the standalone Formance Ledger micro-service only. The ledger runs as a single Go binary against a local PostgreSQL 16 database on the same single VM. It is not the wider Formance Platform: there is no separate gateway, no bundled web console, no message broker and no object storage. The ledger is the state-keeping engine, and you drive it over its HTTP API.
Formance Ledger has no web interface. It is an API server: HTTP requests in, JSON out. Everything in this guide is done with curl and standard HTTP clients, which is what a financial API is meant for. Point your own application at it.
Security by design, deny by default. Because a ledger holds financial records, this appliance refuses first:
- The ledger process and PostgreSQL are both bound to the loopback interface only and are never reachable off the VM.
- The only routable port is 8080, served by an nginx reverse proxy that enforces HTTP Basic authentication against a credential unique to your VM. Every request without that credential is refused with HTTP 401 — including writes.
- Nothing is baked into the image. On first boot each VM generates a unique password for the
postgressuperuser, a unique password for the ledger database role, and a unique API password, and writes them to the root-only file/root/formance-ledger-credentials.txt. The shipped image contains an empty credential store and an empty ledger database.
What is included:
-
Formance Ledger v2.4.12 from the official upstream release, verified against a pinned SHA-256 checksum at build time, running under systemd as
formance-ledger.service(with the async worker embedded in-process) -
PostgreSQL 16 from the official PostgreSQL PGDG repository, bound to loopback, running as
postgresql.service -
nginx as a Basic-auth reverse proxy on port
8080, the only routable interface -
Per-VM passwords generated on first boot and written to a root-only credentials file, with no default login
-
formance-ledger-selfcheck, a helper that verifies the whole appliance — loopback binding, anonymous refusal, correct double-entry, and rejection of invalid transactions — end to end -
Unattended security upgrades left enabled so the appliance keeps receiving patches
Formance is a trademark of Formance Solutions. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by Formance. The name is used only to identify the open-source software that runs in the image.
Prerequisites
-
Active Azure subscription, an SSH public key, and a VNet and subnet in the target region
-
Subscription to this listing on Azure Marketplace
-
A Network Security Group allowing TCP 22 for administration and TCP 8080 for the Ledger API. In production, restrict
8080to your application subnet — the ledger holds financial data.
Recommended virtual machine size: Standard_B2s with 2 vCPU and 4 GB RAM for development and light workloads. For higher transaction rates, choose a larger size such as Standard_D2s_v5 or above.
Deploy the virtual machine
Deploy from the Azure Portal by selecting the image from Azure Marketplace, choosing your VM size, and supplying your SSH public key for the azureuser account. Or deploy from the Azure CLI. These commands run on your own workstation, not on the VM:
az vm create \
--resource-group my-resource-group \
--name my-formance-ledger \
--image <marketplace-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
Then open the API port to your client network (tighten the source range in production):
az vm open-port --resource-group my-resource-group --name my-formance-ledger --port 8080
SSH in as azureuser:
ssh azureuser@<vm-ip>
Confirm the services are healthy
On the first boot the appliance rotates its credentials, runs the ledger schema migrations against the empty database, and starts the API. Confirm all three services are active:
systemctl is-active postgresql formance-ledger nginx
All three report active. The ledger listens on 127.0.0.1:3068 and PostgreSQL on 127.0.0.1:5432 — both loopback only — while nginx exposes the single routable port 8080.

Retrieve your per-VM credentials
Every secret is generated on first boot and written to a root-only file. Read it with:
sudo cat /root/formance-ledger-credentials.txt
The api.user (default ledger) and api.password are the HTTP Basic credentials for the API on port 8080. Keep this file safe; the secrets are unique to your VM and are shown in plain text only here.
The API is deny by default
Every request to the routable API needs the per-VM credential. An anonymous request — read or write — is refused with HTTP 401 before it ever reaches the ledger:
# anonymous write is refused
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H 'Content-Type: application/json' \
-d '{"metadata":{}}' http://localhost:8080/v2/main/transactions
# anonymous read is refused too
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/_info
Both print 401. With the per-VM credential the same server info request succeeds with 200:
curl -s -o /dev/null -w '%{http_code}\n' -u ledger:<API_PASSWORD> http://localhost:8080/_info

Create a ledger and post your first transaction
A ledger is an isolated set of accounts and transactions. Create one named main:
curl -s -u ledger:<API_PASSWORD> -X POST http://localhost:8080/v2/main \
-H 'Content-Type: application/json' -d '{}'
Now post a transaction. Every transaction is a set of postings that move an amount of an asset from a source account to a destination account. The special world account is the external source of funds and is the only account allowed to hold a negative balance. This posting moves USD/2 (US dollars, 2 decimal places) 100 from world into users:alice:
curl -s -u ledger:<API_PASSWORD> -X POST http://localhost:8080/v2/main/transactions \
-H 'Content-Type: application/json' \
-d '{"metadata":{},"postings":[{"amount":100,"asset":"USD/2","source":"world","destination":"users:alice"}]}'
You can also express the same movement in Numscript, the ledger's built-in DSL for multi-account postings:
curl -s -u ledger:<API_PASSWORD> -X POST http://localhost:8080/v2/main/transactions \
-H 'Content-Type: application/json' \
-d '{"metadata":{},"script":{"plain":"send [USD/2 100] (\n source = @world\n destination = @users:alice\n)"}}'
Read the resulting balances. Double-entry means the destination gained exactly what the source lost, and the sum across the transaction is zero:
curl -s -u ledger:<API_PASSWORD> 'http://localhost:8080/v2/main/accounts/users:alice?expand=volumes'
curl -s -u ledger:<API_PASSWORD> 'http://localhost:8080/v2/main/accounts/world?expand=volumes'
users:alice shows a balance of 100 and world shows -100 for USD/2.

Invalid transactions are rejected
Enforcing accounting invariants is the whole point of a ledger. A normal account cannot go implicitly negative: only world may. This transaction tries to move funds out of an account that has never received any, so the ledger rejects it with INSUFFICIENT_FUND and changes no balances:
curl -s -u ledger:<API_PASSWORD> -X POST http://localhost:8080/v2/main/transactions \
-H 'Content-Type: application/json' \
-d '{"metadata":{},"script":{"plain":"send [USD/2 50] (\n source = @users:emptywallet\n destination = @users:bob\n)"}}'
The response carries "errorCode":"INSUFFICIENT_FUND". You can verify the whole appliance — loopback binding, anonymous refusal, a correct double-entry posting, and this rejection — in one command:
sudo formance-ledger-selfcheck

Where to go next
- Model your own money flows in Numscript — see the Numscript documentation — to express fees, splits, and multi-destination sends atomically.
- Add metadata to accounts and transactions to carry your domain identifiers.
- Query balances, volumes and the full immutable transaction log through the
/v2/{ledger}API. - The API is documented in the OpenAPI specification shipped by the upstream project.
Security notes
- The ledger and PostgreSQL are loopback only. Nothing but nginx is reachable from the network, so the only way to the ledger is through Basic authentication on port 8080.
- No default credentials. The
postgressuperuser password, the ledger database password and the API password are all generated uniquely on first boot. The shipped image has an empty credential store and an empty database. - Rotate the API password by regenerating the nginx credential:
printf '%s' 'your-new-password' | sudo htpasswd -i /etc/nginx/formance-ledger.htpasswd ledger && sudo systemctl reload nginx. - In production, restrict TCP 8080 to your application subnet with the Azure NSG, and consider terminating TLS at nginx or a load balancer in front of it.
- Back up PostgreSQL (
sudo -u postgres pg_dump ledger) as you would any system of record; the ledger's data lives entirely in theledgerdatabase. - Unattended security upgrades are left enabled so the OS keeps receiving patches.