InvenTree on Ubuntu 24.04 on Azure User Guide
Overview
InvenTree is an open source inventory management system built for manufacturing and physical-product businesses. It keeps a structured catalogue of the parts you buy and make, tracks how much of each you hold and where, and records the bills of materials that say which components go into which assemblies. Purchase orders, sales orders and build orders sit on top of that, so the stock figures move as the work happens rather than being maintained by hand.
Everything the web interface can do is also available through a complete REST API, and a documented plugin interface lets you extend the system without forking it.
The cloudimg image delivers InvenTree 1.5.5 on Ubuntu 24.04, served over HTTPS, with a fresh database and every secret generated on the first boot of your VM. Backed by 24/7 cloudimg support.
What is included:
- InvenTree 1.5.5 in a dedicated Python virtual environment at
/opt/inventree, run by gunicorn as the unprivilegedinventreeuser and bound to127.0.0.1:6000only - The React web interface, served by nginx over HTTPS on port 443 with a certificate generated for your VM; port 80 only redirects to HTTPS (plus a plain
/healthzfor load balancer probes) - The django-q2 background worker for scheduled tasks, notifications and label printing
- PostgreSQL 16 and Redis 7, both on the loopback address only
- No default login: the administrator password, the database password, the Django secret key and the TLS key are all generated on your VM's first boot and written to a root-only file
postgresql,redis-server,inventree-web-1,inventree-worker-1andnginxsystemd services, enabled and active
InvenTree 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 MIT licence.

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) runs InvenTree comfortably for a small team; choose a larger size for many concurrent users, large catalogues 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
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for InvenTree by cloudimg and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTPS (443). Then select Review + create and Create.
Step 2: Deploy from the Azure CLI
az vm create \
--resource-group my-resource-group \
--name my-inventree \
--image cloudimg:inventree-ubuntu-24-04:default:latest \
--size Standard_B2s \
--storage-sku StandardSSD_LRS \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
Then open the ports the web interface needs:
az vm open-port --resource-group my-resource-group --name my-inventree --port 443 --priority 1001
az vm open-port --resource-group my-resource-group --name my-inventree --port 80 --priority 1002
Step 3: Connect to your VM
ssh azureuser@<vm-public-ip>
The login banner points at the credentials file and the web address. The first boot takes roughly a minute to generate this VM's secrets and initialise the database; if the web interface is not answering yet, give it a moment and retry.
Step 4: Confirm the InvenTree stack is running
All five services should report active:
sudo systemctl is-active postgresql redis-server inventree-web-1 inventree-worker-1 nginx
active active active active active
Confirm the application version and that the background worker has registered:
curl -sk https://127.0.0.1/api/ | head -c 200
{"server": "InvenTree", "id": null, "version": "1.5.5", "instance": "InvenTree", "apiVersion": 530, "worker_running": true, "worker_count": 2, "worker_pending_tasks": 0, "plugins_enabled": true
worker_running: true is the field to check after any restart — it confirms the django-q2 worker is connected to Redis and picking up tasks. debug_mode: false confirms the server is in production mode.

The load balancer health endpoint answers on plain HTTP without a redirect, which is what makes it usable as an Azure health probe:
curl -s http://127.0.0.1/healthz
ok
Everything else on port 80 redirects to HTTPS:
curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}\n' http://127.0.0.1/
301 -> https://127.0.0.1/
Step 5: Check what the network can reach
The application server, the database and the cache are all bound to the loopback interface. The only TCP ports reachable from outside the VM are SSH, HTTP (which redirects) and HTTPS:
sudo /usr/local/sbin/inventree-port-check.sh
off-box TCP exactly [22 80 443]; gunicorn 127.0.0.1:6000; PostgreSQL + Redis loopback
INVENTREE_PORTS_OK

This matters because InvenTree's own application server speaks plain HTTP. Upstream's packaging binds it to every interface, which would expose the whole application on port 6000 without TLS and without the protections nginx adds. On this image it is bound to 127.0.0.1 and nginx is the only way in.
Step 6: Retrieve the first-boot credentials
No password ships in this image. On the first boot your VM generates its own administrator password, database password, Django secret key and TLS certificate, and writes the credentials to a root-only file:
sudo cat /root/inventree-credentials.txt
# InvenTree on Ubuntu 24.04 by cloudimg
# Generated on this VM's first boot. These credentials are unique to THIS VM.
INVENTREE_URL=https://<vm-public-ip>/
INVENTREE_ADMIN_USER=admin
INVENTREE_ADMIN_PASSWORD=<INVENTREE_ADMIN_PASSWORD>
The file is 0600 root:root, so only root can read it:
sudo ls -l /root/inventree-credentials.txt

Copy the password somewhere safe, then change it from the web interface once you have signed in.
Step 7: Sign in
Browse to https://<vm-public-ip>/ and sign in as admin with the password from the file above.
The certificate generated on first boot is self-signed and carries your VM's address, so a browser shows a warning on the first visit. That is expected for an instance reached by IP address; Step 12 covers installing a certificate for a real domain name.

The dashboard starts empty — use the menu in the top right to add the widgets you want. The navigation bar across the top is the main entry point: Parts, Stock, Manufacturing, Purchasing and Sales.
Step 8: Build your parts catalogue
Parts is the catalogue of everything you buy, make or sell. Each part belongs to a category, carries an internal part number (IPN), a description and any custom parameters you define.
To create one: Parts → choose or create a category → New Part. Give it a name, an IPN and a description, and set the flags that describe what it is — Component for something used inside other parts, Assembly for something built from other parts, Purchaseable and Salable as appropriate.

Step 9: Track stock
Stock records how much of each part you hold and where. Stock lives in locations, which nest — a warehouse containing shelves, for example.
Create a location under Stock → Stock Locations → New Location, then add stock against a part with New Stock Item, choosing the part, the location and the quantity. From then on every movement — transfers, counts, consumption by a build — is recorded against that item, so the history explains how you arrived at the current figure.

Step 10: Create a bill of materials
A bill of materials is the list of components that go into an assembly, with the quantity of each. It is what turns a parts list into something you can actually build from.
Open a part that is marked as an Assembly, choose the Bill of Materials tab, and add a line for each component and its quantity. InvenTree shows the available stock for every line, so you can see immediately whether you can build the assembly. Once you are satisfied the list is correct, validate the BOM to record that it has been reviewed.

From there, Manufacturing → Build Orders lets you raise a build against the assembly; InvenTree allocates stock to the build from the BOM and consumes it when the build completes.
Step 11: Use the REST API
Everything in the web interface is available through the REST API. Request a token with your administrator credentials:
curl -sk -u 'admin:<INVENTREE_ADMIN_PASSWORD>' https://127.0.0.1/api/user/token/
{"token":"inv-<your-token>-20260920","name":"","expiry":"2027-09-20"}
Then use that token for subsequent calls:
TOKEN=$(curl -sk -u 'admin:<INVENTREE_ADMIN_PASSWORD>' https://127.0.0.1/api/user/token/ | python3 -c 'import json,sys;print(json.load(sys.stdin)["token"])')
curl -sk -H "Authorization: Token ${TOKEN}" 'https://127.0.0.1/api/part/?limit=3' | head -c 200
Without credentials the API returns 401, so your catalogue is never readable anonymously:
curl -sk -o /dev/null -w '%{http_code}\n' https://127.0.0.1/api/part/
401
The browsable API documentation is at https://<vm-public-ip>/api-doc/.
Step 12: Use a domain name and your own certificate
Point a DNS A record at your VM's public IP, then tell InvenTree its real address. Two things must change together — the site URL InvenTree builds links from, and the origins it will accept form submissions from:
The three keys that must change live in /etc/inventree/config.yaml:
sudo grep -A3 -E '^(site_url|allowed_hosts|trusted_origins)' /etc/inventree/config.yaml
Edit that file to set site_url to https://inventree.your-domain.com, add the same URL to trusted_origins, and add the hostname to allowed_hosts. Then install your certificate and restart:
sudo cp fullchain.pem /etc/ssl/inventree/inventree.crt
sudo cp privkey.pem /etc/ssl/inventree/inventree.key
sudo chmod 0640 /etc/ssl/inventree/inventree.key
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl restart inventree-web-1
If you use Let's Encrypt, certbot --nginx -d inventree.your-domain.com will obtain and install a certificate and adjust the nginx site for you.
Leaving allowed_hosts or trusted_origins without your domain is the usual cause of a login form that appears to do nothing: the page loads, but Django rejects the sign-in POST because the browser's Origin is not trusted.
Step 13: Security model
- No shipped credential. The administrator password, the database password, the Django secret key, the OIDC key and the TLS key pair are all generated on this VM's first boot. Two instances launched from this image never share a secret.
- Nothing serves before initialisation. gunicorn, the background worker and nginx are gated on a marker that first boot writes only after the secrets exist, so there is no window in which the application is reachable with an uninitialised database.
- The application server is not exposed. gunicorn, PostgreSQL and Redis listen on loopback only; nginx terminates TLS and is the single entry point.
- Client addresses cannot be forged. nginx sets
X-Forwarded-ForandX-Real-IPfrom the real connection address rather than appending to whatever the client sent, and clearsForwarded, so a caller cannot make InvenTree's failed-login rate limiting blame a different address. - Uploaded files stay behind authentication.
/media/is served only after an authentication subrequest, while/static/is public — which it must be, or the login page could not render.
The image ships a self-test that proves these properties on your own VM:
sudo /usr/local/sbin/inventree-selftest.sh --customer-vm
--customer-vm adds the checks that only make sense on a deployed instance: that the build-time administrator account is gone, that your own login user exists, and that first boot completed.

Create individual accounts for your team rather than sharing the administrator login: Settings → Users. InvenTree has a role-based permission model, so you can grant access per area (parts, stock, purchasing, sales) and per action.
Step 14: Back up your data
InvenTree's data lives in the PostgreSQL database and in the uploaded files under /opt/inventree/data/media. Back up both together:
sudo -u postgres pg_dump -Fc inventree > /var/tmp/inventree-$(date +%F).dump
sudo tar czf /var/tmp/inventree-media-$(date +%F).tar.gz -C /opt/inventree/data media
Copy both files off the VM. To restore into a fresh instance, stop the application, restore the dump and the media directory, then start it again:
sudo systemctl stop inventree-web-1 inventree-worker-1
sudo -u postgres pg_restore -d inventree --clean --if-exists /var/tmp/inventree-2026-09-20.dump
sudo systemctl start inventree-web-1 inventree-worker-1
An Azure snapshot of the OS disk is a reasonable coarse backup, but a database dump is what lets you restore a single instance without rolling the whole VM back.
Step 15: Logs, upgrades and maintenance
Application logs go to the journal:
sudo journalctl -u inventree-web-1 -n 30 --no-pager
The background worker logs separately:
sudo journalctl -u inventree-worker-1 -n 20 --no-pager
Operating system security updates are applied automatically by unattended-upgrades. Run a Django management command with:
sudo inventree-manage help
InvenTree itself is pinned to 1.5.5 in this image so the version you launch is the version you tested. Upgrading InvenTree in place is a database-migrating operation: take the backups from Step 14 first, then follow the project's own upgrade instructions for the packaged install. If you would rather not run the upgrade yourself, cloudimg support can do it for you.
Memory is the resource to watch on Standard_B2s:
free -h | head -2
If you are running large reports or many background jobs, move to a size with more RAM rather than adding swap — Azure image certification does not permit swap on the OS disk, and this image ships without it.
Support
cloudimg provides 24/7 support for this image: deployment, TLS certificates and custom domains, SSO and LDAP integration, API automation, performance tuning and upgrades. Contact us at cloudimg.co.uk with your Azure subscription ID and the VM name.