Warracker on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of Warracker on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Warracker is a self hosted warranty tracker: a single searchable record of everything you own that came with a warranty, what you paid for it, when the cover runs out, and where the paperwork is.
Each product becomes one entry with its purchase date and price, vendor, serial and model numbers, warranty length or an exact expiry date, notes and tags. Receipts, invoices, manuals and product photos are uploaded and stay attached to that entry for as long as you keep it. The dashboard sorts and filters by expiry so the things about to run out surface on their own, and a status view charts what is active, what is expiring soon and what has lapsed.
Warracker can also tell you before cover lapses rather than after: point it at your own mail server for expiry email, or at Discord, Slack, Telegram, Teams, ntfy, Pushover or a generic webhook through Apprise. It supports multiple users with an administrator role, optional single sign on through any OpenID Connect provider, optional integration with a Paperless-ngx document archive, a light and dark interface, and a translated interface in more than a dozen languages.
The cloudimg image ships the free and open source, AGPL-3.0 licensed Warracker 1.0.2, installed natively on Ubuntu 24.04: nginx serves the interface on port 80 and reverse proxies the API to a gunicorn worker on the loopback address, backed by PostgreSQL 16 which also listens only on loopback. Backed by 24/7 cloudimg support.
Warracker is an independent open source project. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by the Warracker project. It ships the free and open source AGPL-3.0 licensed software, unmodified.

What is included:
- Warracker 1.0.2, the AGPL-3.0 licensed upstream release, pinned by tag and verified against a SHA-256 checksum and the release commit at build time
- The version the code declares is asserted against the pinned version at build time and again after deployment, so a mislabelled upstream tag fails the build rather than shipping
- nginx on port
80serving the interface and reverse proxying/api/, installed as a host package so unattended security upgrades keep the internet facing component patched - gunicorn bound to
127.0.0.1:5000and PostgreSQL 16 bound to loopback, so the only thing exposed to your network is the web interface - Exactly one administrator, seeded on first boot with a password unique to your VM, and public registration closed in the same step
- The Flask session signing key, the PostgreSQL application role password and the PostgreSQL privileged role password all generated per VM on first boot and written to a root only file
- Fail closed guards: both the application service and nginx refuse to start if any secret is empty, unrotated, or a published upstream default
- SMTP, OIDC and Apprise credentials shipped empty — no mail account is ever baked into the image
- Uploaded documents stored
0750under a0750data directory, never served directly by nginx, and reachable only through an authenticated route - The complete corresponding source, its checksum, the AGPL licence text and a written offer at
/usr/share/warracker/source - Ubuntu 24.04 LTS base with latest security patches applied at build time and unattended security upgrades enabled
- Azure Linux Agent for seamless cloud integration and SSH key injection
- 24/7 cloudimg support with guaranteed 24 hour response SLA
Prerequisites
- An Azure subscription with permission to create virtual machines
- An SSH key pair for administrative access to the VM
- A network security group that allows inbound TCP
80from the networks you trust, and TCP22from your administrative network only - A
Standard_B2sVM (2 vCPU, 4 GiB RAM) is sufficient; Warracker, gunicorn and PostgreSQL all run comfortably in that footprint
Step 1: Deploy from the Azure Portal
- In the Azure Portal, search the Marketplace for Warracker on Ubuntu 24.04 LTS by cloudimg and select Create.
- Choose your subscription, resource group and region.
- Set the VM size to Standard_B2s or larger.
- Under Administrator account, select SSH public key and paste your public key. The administrative user is
azureuser. - Under Inbound port rules, allow HTTP (80) and SSH (22). Restrict both to your own address ranges wherever you can.
- On the Disks tab, Standard SSD is a good default for the OS disk.
- Select Review + create, then Create.
First boot takes about a minute: the appliance generates its own secrets, applies the database schema, seeds the administrator and closes registration before the web interface accepts a single request.
Step 2: Deploy from the Azure CLI
Create the network, then the VM. Replace the placeholders with your own values.
# Placeholders: substitute your own resource group, region and address range.
az group create --name my-resource-group --location eastus
az network nsg create --resource-group my-resource-group --name warracker-nsg
az network nsg rule create --resource-group my-resource-group --nsg-name warracker-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 my-resource-group --nsg-name warracker-nsg \
--name allow-http --priority 1010 --destination-port-ranges 80 \
--source-address-prefixes <your-mgmt-cidr> --access Allow --protocol Tcp
# Accept the image terms once per subscription, before the first deployment.
az vm image terms accept --publisher cloudimg --offer warracker-ubuntu-24-04 --plan default
az vm create \
--resource-group my-resource-group \
--name warracker-01 \
--image cloudimg:warracker-ubuntu-24-04:default:latest \
--size Standard_B2s \
--storage-sku StandardSSD_LRS \
--admin-username azureuser \
--generate-ssh-keys \
--nsg warracker-nsg \
--public-ip-sku Standard
Step 3: Connect to your VM
ssh azureuser@<vm-ip>
Step 4: Confirm the appliance is running
Four services make up the appliance. warracker-firstboot is a one shot that provisions this machine's own secrets; it stays active after it has completed because it is a RemainAfterExit unit.
systemctl is-active postgresql warracker-firstboot warracker nginx
Expected output:
active
active
active
active
Confirm the version that is actually deployed, rather than trusting a label:
grep -o "currentVersion = '[^']*'" /var/www/warracker/version-checker.js | head -1
Expected output:
currentVersion = '1.0.2'
Check what is listening. Only :22 and :80 are reachable from outside the VM; the application server and the database are both bound to the loopback address.
ss -tlnH | awk '{print $4}' | sort -u
Expected output:
0.0.0.0:22
0.0.0.0:80
127.0.0.1:5000
127.0.0.1:5432
127.0.0.53%lo:53
127.0.0.54:53
[::]:22
[::]:80
Confirm the interface answers:
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1/
Expected output:
HTTP 200
Step 5: Read your per instance credentials
Every secret on this appliance is generated on this machine during first boot. Nothing is baked into the image, so no two deployments share a credential and cloudimg does not know yours.
sudo cat /root/warracker-credentials.txt
The file is readable only by root:
stat -c '%a %U:%G' /root/warracker-credentials.txt
Expected output:
600 root:root

The file contains the sign in details plus the database credentials:
| Key | What it is |
|---|---|
WARRACKER_URL |
The address this VM resolved for itself at first boot |
WARRACKER_ADMIN_USERNAME |
The administrator sign in name, admin |
WARRACKER_ADMIN_EMAIL |
The administrator email address, used for password reset if you configure mail |
WARRACKER_ADMIN_PASSWORD |
The administrator password, unique to this VM |
WARRACKER_DB_USER / WARRACKER_DB_PASSWORD |
The PostgreSQL role the application uses |
WARRACKER_DB_ADMIN_USER / WARRACKER_DB_ADMIN_PASSWORD |
The privileged PostgreSQL role |
WARRACKER_SECRET_KEY |
The key that signs every session token |
Change the administrator password after your first sign in, from the account menu in the top right of the interface.
Step 6: Sign in
Open http://<vm-ip>/ in a browser. You are sent to the sign in page: this appliance has no anonymous access at all.

Sign in with the WARRACKER_ADMIN_USERNAME and WARRACKER_ADMIN_PASSWORD values from the credentials file.
Note the line under the sign in button: new account registration is currently disabled. That is deliberate, and it is the single most important difference between this image and a stock Warracker deployment. See Step 7.
Step 7: Why nobody else can claim your instance
Warracker's own behaviour is that the first person to register becomes the administrator and the owner of the instance. It will also accept that first registration on an empty database regardless of the registration setting. On a public address, a stock deployment is therefore a race between you and whoever finds the port first.
This image never enters that state. First boot seeds exactly one administrator with the password in your credentials file, and closes public registration in the same step — before the web interface accepts any request at all. Prove it for yourself:
curl -s -o /dev/null -w 'HTTP %{http_code}\n' -X POST \
-H 'Content-Type: application/json' \
-d '{"username":"passerby","email":"a@b.com","password":"Passw0rd1"}' \
http://127.0.0.1/api/auth/register
Expected output — a genuine refusal, not a redirect:
HTTP 403
Unauthenticated API requests are refused too:
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1/api/warranties
Expected output:
HTTP 401
And the published upstream database password does not work on your instance:
PGPASSWORD=warranty_password psql -w -h 127.0.0.1 -U warranty_user -d warranty_db \
-c 'SELECT 1' </dev/null >/dev/null 2>&1 \
&& echo 'ACCEPTED' || echo 'rejected'
The -w is not optional in a probe like this. Without it psql prompts for a
password and reads it from standard input, which in a script is the rest of the
script: the run then ends early and still exits 0, so a test that never ran
reports success.
Expected output:
rejected

The same protection is enforced at start up, not only at first boot. A guard runs before both the application service and nginx, and refuses to let either serve if the signing key or either database password is empty, still the placeholder the image ships, or a value published in Warracker's own source:
sudo /usr/local/sbin/warracker-preflight.sh
Expected output:
warracker-preflight: OK
To add more users later, sign in as the administrator, open Settings, and re enable registration — or create the accounts yourself from the admin area.
Step 8: Record a warranty with its paperwork
Select Add New Warranty. The dialog walks through product details, the warranty term, documents, tags and a summary.

- Product — name, an optional link to the product page, one or more serial numbers, model number and vendor
- Warranty — purchase date and price, then either a duration in years, months and days, an exact expiry date, or a lifetime warranty
- Documents — the receipt or invoice, the manual, a product photo, and any other supporting file (PDF, PNG, JPG)
- Tags — free form labels you can filter on later
Saved entries appear on the dashboard, colour coded by how much cover is left and sorted so the ones about to expire come first.

Each card carries links to the documents attached to it and a countdown of the days remaining on the cover.

The Status view charts the whole collection: how many warranties are active, how many are expiring soon, how many have lapsed, and when the remaining ones run out.

Step 9: Your uploaded documents are private
Receipts, invoices, manuals and product photos are user data and are treated as such on this image.
The upload directory is not world readable:
stat -c '%a %U:%G %n' /var/lib/warracker /var/lib/warracker/uploads
Expected output:
750 warracker:warracker /var/lib/warracker
750 warracker:warracker /var/lib/warracker/uploads
nginx refuses to serve the upload paths directly, so a document can never be fetched by guessing its filename:
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1/uploads/anything.pdf
Expected output:
HTTP 403
The only route to a stored document is /api/secure-file/, which requires a valid session and checks that the session owns the warranty the document belongs to. An unauthenticated request is refused:
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1/api/secure-file/example.pdf
Expected output:
HTTP 401
Step 10: Expiry email and push notifications
Warracker can send expiry reminders by email and push them to a long list of services. Both ship switched off with empty credentials, because cloudimg never bakes a mail account into an image.
To enable email, add your own mail server details to the environment file and restart the service. This edits configuration, so run it only when you have real values to hand:
# Replace every placeholder with your own mail server details before running this.
sudo sed -i \
-e 's|^SMTP_HOST=.*|SMTP_HOST=smtp.<your-domain>|' \
-e 's|^SMTP_USERNAME=.*|SMTP_USERNAME=<your-email>|' \
-e 's|^SMTP_PASSWORD=.*|SMTP_PASSWORD=your-mail-app-password|' \
-e 's|^SMTP_FROM_ADDRESS=.*|SMTP_FROM_ADDRESS=<your-email>|' \
/etc/warracker/warracker.env
sudo systemctl restart warracker
For push notifications, sign in as the administrator and open Settings to enable Apprise and add your notification URLs — for example discord://webhook_id/webhook_token, telegram://BotToken/ChatID or ntfy://topic. You can also set them in the same environment file:
# Replace the webhook id and token with your own before running this.
sudo sed -i -e 's|^APPRISE_ENABLED=.*|APPRISE_ENABLED=true|' \
-e 's|^APPRISE_URLS=.*|APPRISE_URLS=discord://webhook_id/<your-token>|' \
/etc/warracker/warracker.env
sudo systemctl restart warracker
The reminder schedule (APPRISE_EXPIRATION_DAYS, default 7 and 30 days before expiry, and APPRISE_NOTIFICATION_TIME, default 09:00) lives in the same file.
Step 11: Where your data lives
| Path | Contents |
|---|---|
/var/lib/warracker/uploads |
Uploaded receipts, invoices, manuals and product photos |
PostgreSQL database warranty_db |
Warranty records, users, tags, settings and audit log |
/etc/warracker/warracker.env |
Runtime configuration and this VM's secrets, mode 0640 root:warracker |
/root/warracker-credentials.txt |
The first boot credential summary, mode 0600 root:root |
/opt/warracker/app |
The deployed backend, migrations and translation catalogues |
/var/www/warracker |
The deployed frontend served by nginx |
/usr/share/warracker/source |
AGPL corresponding source, its checksum, the licence and the written offer |
A complete backup is the database plus the upload directory. This block writes a dump, so run it deliberately rather than as part of a walk through:
# Backs up the database and the uploaded documents to <backup-dir>.
sudo -u postgres pg_dump warranty_db | gzip > <backup-dir>/warranty_db.sql.gz
sudo tar czf <backup-dir>/warracker-uploads.tar.gz -C /var/lib/warracker uploads
Step 12: The complete source is on the machine
Warracker is licensed under the GNU Affero General Public License v3. Section 13 of that licence gives anyone who interacts with the software over a network the right to its corresponding source. That source ships on this image, unmodified, with a checksum you can verify at any time:
ls -1 /usr/share/warracker/source/
Expected output:
LICENSE
WRITTEN-OFFER.txt
warracker-1.0.2-source.tar.gz
warracker-1.0.2-source.tar.gz.sha256
Verify it:
cd /usr/share/warracker/source && sha256sum -c warracker-1.0.2-source.tar.gz.sha256
Expected output:
warracker-1.0.2-source.tar.gz: OK

WRITTEN-OFFER.txt records the upstream project, the exact version and the exact git commit this image was built from, and cloudimg's standing offer to supply the corresponding source to anyone who asks. The deployed tree itself, under /opt/warracker/app and /var/www/warracker, is interpreted Python and JavaScript, which is to say it is source too.
Step 13: Day to day operation
Restart the application after a configuration change:
sudo systemctl restart warracker
Follow the application log:
sudo journalctl -u warracker -n 30 --no-pager
Check the database is healthy:
sudo -u postgres psql -w -d warranty_db -c 'SELECT COUNT(*) AS warranties FROM warranties;' </dev/null
Security updates are applied automatically. Confirm unattended upgrades are enabled:
cat /etc/apt/apt.conf.d/20auto-upgrades
Expected output:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
Security hardening
- Restrict inbound
80and22in the network security group to the address ranges you actually use. Warracker holds purchase prices and scanned receipts. - Change the administrator password after the first sign in, from the account menu.
- Put HTTPS in front of it. The appliance serves plain HTTP on port
80so that it works the moment it boots. For anything beyond a private network, terminate TLS with a certificate for a real hostname — either with an Azure Application Gateway or Front Door in front of the VM, or with certbot on the VM itself once a DNS name points at it:
bash
# Requires a DNS name already pointing at this VM.
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d <your-domain>
After enabling HTTPS, update FRONTEND_URL and APP_BASE_URL in /etc/warracker/warracker.env so that links in reminder email point at the right address.
- Keep registration closed unless you are actively adding users, and prefer creating accounts yourself from the admin area.
- Back up regularly — the database and the upload directory together, per Step 11.
- Leave the secrets alone. The signing key and both database passwords are generated for this VM. If you rotate them by hand, rotate them in /etc/warracker/warracker.env and in PostgreSQL together, then restart; the start up guard will refuse to serve on an inconsistent or empty value rather than fall back to a published default.
Troubleshooting
The web interface returns 502. gunicorn is not running. Check systemctl is-active warracker and journalctl -u warracker -n 50 --no-pager. If the start up guard rejected the configuration it says so explicitly in the log.
The web interface returns nothing and nginx is inactive. Both nginx and the application are held until first boot has finished writing this VM's secrets. Check systemctl status warracker-firstboot — if it failed, journalctl -u warracker-firstboot gives the reason, and sudo systemctl restart warracker-firstboot re runs it safely.
Sign in fails. Confirm you are using the values from /root/warracker-credentials.txt and not a password from another instance. A wrong password returns 401.
Registration is refused and you want more users. That is by design; see Step 7. Re enable registration in Settings, add the users, then switch it off again.
A document will not download. Documents are only served to a session that owns the warranty. Confirm you are signed in as the right user, and check journalctl -u warracker for the SECURE_FILE log lines, which record exactly which path was requested.
Time or expiry dates look wrong. Set the instance timezone from Settings, and confirm the VM clock with timedatectl.
Support
cloudimg provides 24/7 support with a guaranteed 24 hour response SLA for this image, covering deployment, configuration and operation of the appliance.
- Email: support@cloudimg.co.uk
- Guides: https://www.cloudimg.co.uk/guides/
Warracker itself is an independent open source project; product feature requests and upstream bug reports belong at https://github.com/sassanix/Warracker.