Unstract on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and operation of Unstract on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Unstract is an open source, AGPL-3.0 licensed platform for turning unstructured documents (PDFs, scanned pages, Word and Excel files, images) into structured JSON with large language models. You describe the fields you want in Prompt Studio, check the answers against sample documents, and then deploy the result as a REST API or an ETL pipeline that processes documents from a storage connector.
The image runs the whole stack on one VM, and it works with no API key out of the box:
- Unstract (backend, frontend, platform service, text-extraction service and its workers) runs in Docker containers from the official images, each pinned by its SHA-256 digest
- PostgreSQL (with
pgvector), Valkey, RabbitMQ, MinIO and Qdrant hold the platform's data, queues, files and vectors, on a private Docker network that has no route to the internet - Unstructured's open source document parser extracts text locally, including OCR for scanned pages, on the VM's CPU
- Ollama serves two local models on the CPU: IBM Granite 4.0 Tiny answers your extraction prompts and Nomic Embed Text v1.5 creates the embeddings used for retrieval. They are already registered in Unstract as the default adapters
- nginx terminates HTTPS with a certificate generated for this VM
| Component | Version | Licence |
|---|---|---|
| Unstract (backend, frontend, platform service, x2text service, workers) | v0.193.0 | AGPL-3.0 |
| Unstructured API (document parser, built for CPU from its public source) | 0.1.15 | Apache 2.0 |
| Ollama (CPU) | 0.40.5 | MIT |
IBM Granite 4.0 Tiny (granite4:tiny-h) |
Ollama manifest 1da8cadb |
Apache 2.0 |
Nomic Embed Text v1.5 (nomic-embed-text) |
Ollama manifest 8688dbc0 |
Apache 2.0 |
| PostgreSQL 15 + pgvector | 0.8.7 | PostgreSQL Licence |
| Valkey | 8.1.10 | BSD-3-Clause |
| RabbitMQ | 4.3.6 | MPL-2.0 |
| MinIO | RELEASE.2026-09-22 | AGPL-3.0 |
| Qdrant | 1.16.3 | Apache 2.0 |
| Docker Engine / Compose | 29.9.1 / 5.6.0 | Apache 2.0 |
About speed on a CPU. The local models make the image work the moment it starts, but they run on the VM's processor, not a GPU. On a Standard_D4s_v3 (4 vCPU, 16 GiB) we measured a one page invoice with four prompts at about 40 seconds once the models are loaded, and the first extraction after a reboot takes up to about 2 minutes while the models load from disk. Time grows with document length and the number of prompts, and scanned pages are slower than text PDFs because they go through OCR. That is fine for building and testing extractions and for modest volumes. For high volumes, connect a hosted model such as Azure OpenAI in Settings (Step 10); Unstract lets you choose the model per project.
What is not included. Upstream Unstract also has a Docker-socket "runner" that launches the File Classifier and Text Extractor container tools. Unstract v0.193.0 registers versions of those two tool images that were never published, so they cannot run on a self-hosted install, and the runner needs root-equivalent access to Docker. This image therefore ships without the runner and those two tools; everything you build in Prompt Studio, and the API deployments and ETL pipelines built from it, runs inside Unstract's executor and is fully available. Features of Zipstack's commercial Unstract Cloud (for example LLMWhisperer, human review and single sign-on) are not part of the open source edition.
No password, key or certificate is baked into the image. First boot generates, for this VM only, the sign-in password, Django's secret key, the encryption key that protects stored adapter credentials, the PostgreSQL, Valkey, RabbitMQ, MinIO and Qdrant credentials, the internal service key, the key for the local model gateway and the TLS certificate. Nothing answers on the network until first boot has done this, and first boot proves that Unstract's published default login unstract / unstract is refused before it marks the VM ready. Backed by 24/7 cloudimg support.
Unstract is developed by Zipstack Inc. under the AGPL-3.0. Unstructured is developed by Unstructured Technologies, Ollama by Ollama Inc., Granite by IBM and Nomic Embed by Nomic AI, under the licences listed above. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by Zipstack Inc., the Unstract project, Unstructured Technologies, Ollama, IBM or Nomic AI. The names are used here solely to identify the software this image contains.

What is included:
- Unstract v0.193.0 from the official images, pinned by digest. The build proves every image carries only the public source of that release (no closed plugins copied in) and that no NVIDIA/CUDA binary ships
- Only the Unstract UI and API are published, on
127.0.0.1; nginx serves them on443with a per-VM certificate, sends80to HTTPS and limits sign in attempts - The data services, and the document parser, live on Docker networks with no egress at all
- Every container is blocked from the Azure Instance Metadata Service and the Azure wire server, so a URL entered in an LLM, connector or webhook setting cannot read the VM's identity or tokens
- Ollama runs in its own network namespace; containers reach it only through a gateway that requires this VM's key and allows only inference calls
- PostHog product analytics in the UI are switched off, and a same-origin Content Security Policy blocks any third-party request from the browser
- Unattended security upgrades left on for the operating system
Prerequisites
- An Azure subscription with permission to create VMs
- A
Standard_D4s_v3VM (4 vCPU, 16 GiB) or larger is recommended. The stack uses about 5 GiB and the two local models about 9 GiB once loaded, so 16 GiB is the minimum. Prefer a non-burstable size: on a burstableStandard_B4mswe measured continuous extraction using CPU credits faster than they are earned, so after roughly an hour and a half of sustained work the VM is throttled and extractions slow down several times. A B-series size is fine for occasional, interactive use - Inbound TCP
443(and80for the redirect) open only to the networks you connect from, plus22for SSH administration - An SSH key pair for administrative access
- Optional: an API key or endpoint for a hosted model (Azure OpenAI, OpenAI, Anthropic, Google Gemini, Mistral, Bedrock and others) if you want faster or stronger extraction than the local models
Step 1: Deploy from the Azure Portal
- In the Azure Portal, search the Marketplace for Unstract and select the cloudimg offer, then Create.
- Choose your subscription, resource group and region.
- Set the VM size to Standard_D4s_v3 or larger.
- Under Administrator account, select SSH public key and supply your own key.
- Under Inbound port rules, allow HTTPS (443), HTTP (80) and SSH (22), then restrict all three to your own address ranges on the networking blade.
- Review and create.
Step 2: Deploy from the Azure CLI
az vm image terms accept --urn cloudimg1647283583153:unstract-ubuntu-24-04:default:latest
az vm create \
--resource-group my-resource-group \
--name my-unstract \
--image cloudimg1647283583153:unstract-ubuntu-24-04:default:latest \
--size Standard_D4s_v3 \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_rsa.pub \
--public-ip-sku Standard
# Open HTTPS (and the HTTP redirect) to YOUR network only.
MY_CIDR="203.0.113.0/24" # replace with your own range
az network nsg rule create \
--resource-group my-resource-group \
--nsg-name my-unstractNSG \
--name allow-https-from-office --priority 1010 \
--source-address-prefixes "${MY_CIDR}" \
--destination-port-ranges 80 443 --protocol Tcp --access Allow
Step 3: Connect to your VM
ssh azureuser@<vm-ip>
Step 4: Confirm the services are running
First boot takes three to five minutes: it generates this VM's secrets and certificate, initialises the database, proves the login gate, registers the local models as Unstract's default adapters and only then starts nginx. Check that it has finished and that every service is up:
test -f /var/lib/cloudimg/unstract-firstboot.done && echo "first boot complete"
systemctl is-active unstract-netguard docker ollama unstract-model-gateway unstract nginx
Expected output:
first boot complete
active
active
active
active
active
active
All 22 containers of the stack should be running:
sudo docker compose -f /opt/unstract/docker-compose.yml ps --format '{{.Service}} {{.State}}' | sort
backend running
db running
frontend running
minio running
platform-service running
qdrant running
rabbitmq running
redis running
unstructured-io running
worker-log-history-scheduler-v2 running
worker-log-stream-consumer running
worker-pg-callback running
worker-pg-executor running
worker-pg-fileproc running
worker-pg-ide-callback running
worker-pg-metrics running
worker-pg-notification running
worker-pg-orchestrator-api running
worker-pg-orchestrator-general running
worker-pg-reaper running
worker-pg-scheduler running
x2text-service running
Only SSH (22) and nginx (80, 443) listen on public addresses. The Unstract UI (3000) and API (8000) listen on 127.0.0.1, the model gateway (11434) and the DNS resolver for the containers (53) on the private Docker bridge address 172.31.250.1, and 11435 is the local entry into Ollama's private network namespace:
sudo ss -lntH | awk '{print $4}' | grep -vE '^127\.0\.0\.5[34]' | sort -u
0.0.0.0:22
0.0.0.0:443
0.0.0.0:80
127.0.0.1:11435
127.0.0.1:3000
127.0.0.1:8000
172.31.250.1:11434
172.31.250.1:53
[::]:22
[::]:443
[::]:80
Step 5: Read this VM's login
sudo cat /root/unstract-credentials.txt
# Unstract - per-VM credentials generated on first boot (2026-10-16T11:31:19Z).
# Keep this file private. Sign in at UNSTRACT_URL. Change the password with: sudo unstract-set-password
UNSTRACT_URL=https://<vm-ip>/
UNSTRACT_USERNAME=unstract
UNSTRACT_PASSWORD=<UNSTRACT_PASSWORD>
# Bundled local models (CPU) are already registered in Unstract as the default adapters.
# Model gateway for the containers (OpenAI-compatible, needs this key):
LOCAL_MODEL_GATEWAY_URL=http://172.31.250.1:11434/v1
LOCAL_MODEL_GATEWAY_KEY=<LOCAL_MODEL_GATEWAY_KEY>
The sign in user is unstract, but the password is a 24 character value generated for this VM. Unstract's published default password does not work on this image: first boot checks that it is refused, together with a wrong password, before the VM is marked ready.

Step 6: Sign in
Open https://<vm-ip>/ in your browser. The certificate is self-signed for this VM, so your browser shows a warning the first time: confirm the certificate and continue (Step 13 shows how to use your own domain and a trusted certificate). Select Login and sign in as unstract with the password from Step 5. You land in Prompt Studio.
Step 7: Check the local models
Open Settings (your avatar, top right) and then Default LLM Profile. The four adapters every Prompt Studio project needs are already configured and selected as the defaults:
- LLM: Granite 4.0 Tiny (local, CPU)
- Embedding: Nomic Embed Text v1.5 (local, CPU)
- Vector DB: Qdrant (local)
- Text Extractor: Unstructured (local, CPU)

The containers reach the local models through a gateway on the private bridge that requires this VM's key. Without the key it refuses the request:
curl -s -o /dev/null -w '%{http_code}\n' http://172.31.250.1:11434/v1/models
401
With the key from the credentials file (passed to curl on standard input, so it never appears in the process list) it lists both models:
sudo sed -n 's/^LOCAL_MODEL_GATEWAY_KEY=\(.*\)/header = "Authorization: Bearer \1"/p' /root/unstract-credentials.txt \
| curl -s -K - http://172.31.250.1:11434/v1/models | python3 -m json.tool | grep '"id"'
"id": "nomic-embed-text:latest",
"id": "granite4:tiny-h",
Step 8: Extract data from a document in Prompt Studio
The image includes a sample invoice so you can try an extraction straight away. Copy it to your computer with scp azureuser@<vm-ip>:/usr/share/unstract-appliance/samples/sample-invoice.pdf . or use one of your own documents.
- In Prompt Studio, select New Project, give it a name (for example Supplier invoices), an author and a description, and Save.
- Select Manage Documents, upload the PDF and close the dialog. The document appears on the right in PDF View.
- Select + Prompt for each field you want. Click the prompt's name to rename it to the JSON key you want (for example
invoice_number), type the question in the prompt box (for example What is the invoice number? Reply with the invoice number only.) and choose the output type (text,number,date,jsonand others). - Select the Run button on a prompt, or Run All at the top of the list. The answer appears under each prompt.

Combined Output shows the whole result as the JSON document your API or pipeline will return, next to the source:

You can check the whole path from the command line at any time. unstract-selftest runs the sample invoice through a temporary Prompt Studio project and a temporary API deployment on the local models, compares every field with the invoice and then removes both:
sudo unstract-selftest
Extracting /usr/share/unstract-appliance/samples/sample-invoice.pdf with the bundled local models (CPU, 1-3 minutes)...
invoice_number = 'INV-2026-0042' -> OK
invoice_date = '2026-09-30' -> OK
supplier_name = 'Brightdesk Office Supplies Ltd' -> OK
total_due = '1650.0' -> OK
EXTRACTION_OK: 4/4 fields match the source invoice (30 s on the bundled CPU models)
DEPLOYED_API_OK: missing key refused, wrong key refused, 4/4 fields correct via the API (20 s)

Step 9: Deploy your project as a REST API
- In your Prompt Studio project, select Deploy as API and Proceed.
- Enter a display name, a description and an API Name (for example
supplier_invoices), select Next, review the tool settings and select Create. Unstract confirms the deployment; select View API Deployment. - On the deployment's card in API Deployments, open the menu (the three dots) and select Manage Keys to copy its API key (you can add and revoke keys there too). Code Snippets in the same menu gives ready-made cURL, Python and JavaScript examples.

Call it from any machine that can reach the VM, sending a document as files and the key as a bearer token. timeout=300 makes the call wait for the result instead of returning a status URL. Replace <vm-ip> with your VM's address and <API_KEY> with your key (-k is only needed while you use the self-signed certificate):
curl -sk -X POST "https://<vm-ip>/deployment/api/mock_org/supplier_invoices/" \
-H "Authorization: Bearer <API_KEY>" \
-F "files=@sample-invoice.pdf" -F "timeout=300"
The extracted fields are under result[].result.output:
{
"message": {
"execution_status": "COMPLETED",
"result": [
{
"file": "sample-invoice.pdf",
"status": "Success",
"result": {
"output": {
"total_due": 1650.0,
"invoice_date": "2026-09-30",
"supplier_name": "Brightdesk Office Supplies Ltd",
"invoice_number": "INV-2026-0042"
}
},
...
A request without a key is refused with 403 and a wrong key with 401. In the API Deployments list you can disable a deployment with its toggle, rotate keys and see every run under Logs. To process documents in bulk from cloud storage instead, create an ETL Pipeline from the same project.
Step 10: Use a hosted model
The local models are a good default, but a hosted model is faster and handles long or complex documents better.
- Open Settings, then LLMs, and select New LLM Profile. Choose your provider (Azure OpenAI, OpenAI, Anthropic, Google Gemini, Mistral, Bedrock, Ollama or any OpenAI-compatible endpoint), enter its details and key, Test Connection and Submit. Add an embedding model the same way under Embedding if you want hosted embeddings.
- Use it in a single project from the project's LLM Profiles settings (the gear icon), or for every new project from Settings, Default LLM Profile.
Keys you enter for adapters are stored encrypted with this VM's own encryption key. The containers that call hosted models can reach the internet, but never the Azure metadata service, the wire server or the VM's own SSH and admin ports.
To add a different local model, pull it with sudo unstract-model-pull <model> (for example granite4:micro), then create an OpenAI Compatible LLM adapter with the base URL http://172.31.250.1:11434/v1, the key from /root/unstract-credentials.txt and the model name. Use Apache 2.0 or MIT licensed models if you redistribute your results commercially.
Step 11: Understand the security model
Sign in. Unstract's open source edition has one user, unstract, whose password comes from /etc/unstract/backend.env. First boot sets it to a random value and proves the default is refused. nginx allows 10 sign in attempts per minute per address. The session cookie is Secure and HttpOnly; Django's CSRF cookie is Secure but readable by the UI's JavaScript, which is how Unstract's CSRF protection is designed. Django runs with debug mode off.
Network. The table below is what the network guard (sudo nft list table inet unstract_guard) and the Docker network layout enforce:
| From | Can reach | Cannot reach |
|---|---|---|
| The internet | nginx on 443 (80 redirects), SSH on 22 | everything else |
| Unstract app containers | the internet (for hosted models and connectors), the model gateway with the VM's key, the data services | Azure IMDS 169.254.169.254, the wire server 168.63.129.16, the VM's SSH and nginx, Ollama directly |
| Database, queue, storage and vector containers | each other and the app containers | anything outside their network, the internet included |
| Document parser container | the x2text service only | anything else |
You can see it from inside a container:
sudo docker exec unstract-backend python3 -c '
import urllib.request
for u in ["http://169.254.169.254/metadata/instance?api-version=2021-02-01", "http://168.63.129.16/?comp=versions"]:
try:
urllib.request.urlopen(urllib.request.Request(u, headers={"Metadata": "true"}), timeout=5); print(u, "REACHABLE")
except Exception as e:
print(u.split("/")[2], "blocked")'
169.254.169.254 blocked
168.63.129.16 blocked

Analytics. The Unstract UI includes PostHog product analytics that are on by default upstream. This image switches them off, and nginx sends a Content Security Policy that only allows the VM's own origin, so the browser cannot send analytics or load scripts from anywhere else even if a setting changed:
sudo curl -s --cacert /etc/unstract/tls/server.crt https://127.0.0.1/config/runtime-config.js | grep -i posthog
enablePosthog: "false",
Step 12: Change the sign in password
sudo unstract-set-password
It prompts twice for the new password (16 characters or more), updates /etc/unstract/backend.env and /root/unstract-credentials.txt, and restarts the backend container, which takes about a minute.
Step 13: Use your own domain and a trusted certificate
- Create a DNS record for your domain pointing at the VM's public IP, and allow inbound
80while you request the certificate. - Install certbot and get a certificate with nginx (replace
unstract.example.com):
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d unstract.example.com
- Tell Unstract its new address, so sign in redirects and CSRF checks use your domain. Edit
/etc/unstract/backend.envand setWEB_APP_ORIGIN_URLandDJANGO_APP_BACKEND_URLtohttps://unstract.example.com, then recreate the backend:
sudo nano /etc/unstract/backend.env
sudo docker compose -f /opt/unstract/docker-compose.yml up -d --force-recreate --no-deps backend
Step 14: Day-2 operations
Logs. Each part of the stack logs to Docker; for example the backend and the executor that runs your prompts:
sudo docker logs --tail 5 unstract-backend 2>&1 | cut -c1-120
Restart the whole stack (data is kept in /var/lib/unstract):
sudo systemctl restart unstract
Versions. Every pinned component is recorded on the VM:
grep -E '^(unstract|ollama|chat_model|embed_model)=' /usr/share/unstract-appliance/VERSIONS | cut -c1-90
unstract=v0.193.0
ollama=v0.40.5 (sha256 dc5eabc292f9949546c90a97c820f924b035e05243b0de13c7f366182562169c)
chat_model=granite4:tiny-h (manifest sha256 1da8cadb9b69a0ff5ec7d6a08f4f7dcd127f663f2932ba
embed_model=nomic-embed-text:latest (manifest sha256 8688dbc0926c1dfdc1b8b7489034e1fedb06f
Backups. Your projects, deployments, uploaded documents and vectors live in /var/lib/unstract (PostgreSQL, MinIO, Qdrant and Prompt Studio files) and the per-VM secrets in /etc/unstract. Back up both together, with the stack stopped (sudo systemctl stop unstract), or snapshot the VM's disk. Keep /etc/unstract/backend.env with the data: its ENCRYPTION_KEY is needed to read the adapter keys stored in the database.
Updates. Ubuntu security updates install automatically. The Unstract images are pinned for reproducibility; cloudimg publishes updated images when new Unstract releases are validated.
Source code. Unstract and MinIO are AGPL-3.0. The exact source of the Unstract release in this image is the public repository at commit 6da459096d67be1ae89e47621f83a15f332bfefa (tag v0.193.0) at https://github.com/Zipstack/unstract; the image runs it unmodified, with configuration only (/opt/unstract). The document parser is built from https://github.com/Unstructured-IO/unstructured-api at commit 448352104eefd2e5b8fa2dc32282fc1cd3db31ae; the image ID and the pinned package list are in /usr/share/unstract-appliance.
Support
cloudimg provides 24/7 support for this image. Contact support@cloudimg.co.uk with your VM's region, size and a description of the issue.