Deployment guide

Unstract

Overview

This guide covers deploying and operating Unstract on Ubuntu 24.04 on Azure, using SecureAnyCloud Azure Marketplace images. Unstract is an open-source, AGPL-3.0 licensed platform that turns 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 and check the answers against sample documents. Then you deploy the result as a REST API or as an ETL pipeline that processes documents from a storage connector.

What you get:

  • The whole stack on one VM, working 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 on the VM's CPU, including OCR for scanned pages.
  • 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. Both are already registered in Unstract as the default adapters.
  • nginx terminates HTTPS with a certificate generated for this VM.

Component versions and licenses:

  • 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 License
  • 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. 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. 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. The runner also 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 the following 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 service-to-service key, the key for the local model gateway and the TLS certificate.

Nothing answers on the network until first boot has done this. First boot also proves that Unstract's published default login, unstract / unstract, is refused before it marks the VM ready. Backed by 24/7 SecureAnyCloud 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 licenses listed above. This image is produced by SecureAnyCloud 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 that 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 on 443 with a per-VM certificate, sends 80 to 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 are left on for the operating system.

Prerequisites

  • An Azure subscription with permission to create VMs.
  • A Standard_D4s_v3 VM (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 burstable Standard_B4ms, we measured continuous extraction using CPU credits faster than they are earned. 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 (and 80 for the redirect) open only to the networks you connect from, plus 22 for 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.
1

Deploy from the Azure Portal

  • In the Azure Portal, search the Marketplace for Unstract, select the SecureAnyCloud offer, and select 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.
  • Select Review + create.
2

Deploy from the Azure CLI

az vm image terms accept --urn sac1647283583153:unstract-ubuntu-24-04:default:latest
az vm create \
  --resource-group my-resource-group \
  --name my-unstract \
  --image sac1647283583153: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
3

Connect to your VM

ssh azureuser@<vm-ip>
4

Confirm the services are running

First boot takes three to five minutes. It generates this VM's secrets and certificate, initializes 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/sac/data/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) listen on the private Docker bridge address 172.31.250.1. Port 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
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.

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.

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, the gateway lists both models. The key is passed to curl on standard input, so it never appears in the process list:

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",
8

Extract data from a document in Prompt Studio

The image includes a sample invoice so you can try an extraction right 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, then select 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.). Then choose the output type (text, number, date, json and 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, next to the source, as the JSON document your API or pipeline will return.

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. It compares every field with the invoice and then removes both.

It takes one to three minutes. The very first run after you deploy the VM can take up to about five, because the models are still being read from the new disk:

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 (35 s on the bundled CPU models) DEPLOYED_API_OK: missing key refused, wrong key refused, 4/4 fields correct via the API (24 s)
9

Deploy your project as a REST API

  • In your Prompt Studio project, select Deploy as API, then 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. Send 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. The -k flag 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.

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) and enter its details and key. Select Test Connection, then Submit. To use hosted embeddings, add an embedding model the same way under Embedding.
  • Use the profile in a single project from the project's LLM Profiles settings (the gear icon). To use it for every new project, set it under 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.

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 list below is what the network guard (sudo nft list table inet unstract_guard) and the Docker network layout enforce:

  • The internet can reach nginx on 443 (80 redirects) and SSH on 22. It cannot reach anything else.
  • Unstract app containers can reach the internet (for hosted models and connectors), the model gateway with the VM's key, and the data services. They cannot reach Azure IMDS 169.254.169.254, the wire server 168.63.129.16, the VM's SSH and nginx, or Ollama directly.
  • Database, queue, storage and vector containers can reach each other and the app containers. They cannot reach anything outside their network, the internet included.
  • The document parser container can reach the x2text service only. It cannot reach 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, which are on by default upstream. This image switches them off. nginx also sends a Content Security Policy that allows only 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",
12

Change the sign in password

sudo unstract-set-password

The command prompts twice for the new password (16 characters or more). It updates /etc/unstract/backend.env and /root/unstract-credentials.txt, then restarts the backend container, which takes about a minute.

13

Use your own domain and a trusted certificate

  • Create a DNS record for your domain that points at the VM's public IP. Allow inbound 80 while 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.env and set WEB_APP_ORIGIN_URL and DJANGO_APP_BACKEND_URL to https://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
14

Day-2 operations

Logs. Each part of the stack logs to Docker. For example, view 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). The per-VM secrets live 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. SecureAnyCloud 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

SecureAnyCloud provides 24/7 support for this image. Contact sales@secureanycloud.com with your VM's region, size and a description of the issue.

Deploy on Azure

Launch Unstract on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure