Unstract on AWS User Guide
Overview
This guide covers launching and using the Unstract 0.203.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg MLOps and Speech AI listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
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.
Not included: the Snowflake connector. Its client library has a critical security flaw with no fix that works with this Unstract release, so this image leaves it out; every other upstream connector is present.
The image runs the whole stack on one instance, 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 v0.203.0 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 instance'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 instance
| Component | Version | Licence |
|---|---|---|
| Unstract (backend, frontend, platform service, x2text service, workers) | v0.203.0 | AGPL-3.0 |
| Unstructured API (document parser, built for CPU from its public source) | 0.1.16 | Apache 2.0 |
| Ollama (CPU) | 0.40.15 | MIT |
IBM Granite 4.0 Tiny (granite4:tiny-h) |
Ollama manifest 566b7255 |
Apache 2.0 |
Nomic Embed Text v1.5 (nomic-embed-text) |
Ollama manifest 0a109f42 |
Apache 2.0 |
| PostgreSQL 15 + pgvector | 0.8.7 | PostgreSQL Licence |
| Valkey | 8.1.11 | BSD-3-Clause |
| RabbitMQ | 4.3.7 | MPL-2.0 |
| MinIO | Unstract's own pinned image (digest bd014394) |
AGPL-3.0 |
| Qdrant | 1.16.3 | Apache 2.0 |
| Docker Engine / Compose | 29.9.3 / 5.7.0 | Apache 2.0 |
Upstream Unstract signs everyone in with a published default login, unstract / unstract. On this image that login never works:
- No default password, no first caller takeover. First boot generates, for this instance 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 the published default login is refused before it lets nginx start.
- HTTPS from the first boot. nginx serves the web interface and the API on port 443. Port 80 redirects to HTTPS. Sign in is rate limited per IPv4 address and per IPv6 /64.
- An egress fence for every container. Unstract fetches what you point it at: model endpoints, storage connectors, webhooks. Every container is refused the instance metadata service (
169.254.169.254andfd00:ec2::254), link local and reserved ranges, loopback, the instance's own SSH and admin ports and your VPC's own address blocks, so an address typed into an adapter or connector setting, a host name that resolves to an internal address, or a redirect to one, cannot reach your instance's credentials or your private network. Public destinations stay reachable for hosted models and cloud storage, and you can allow a private service in one file (Step 12). - Request hygiene at nginx. Cross origin requests that change data are refused, forwarding headers a client sends are replaced, malformed
Content-Typecharsets, compressed request bodies and oversizedAcceptheaders are refused before they reach Django, and internal routes answer404. - Patched beyond upstream. Fifteen Python dependencies are raised to fixed releases within Unstract's own version ranges, the web interface's PDF viewer has its font
evalpath switched off (CVE-2024-4367), and the Snowflake connector is left out (see above). Ubuntu 24.04 LTS is fully patched at build time and unattended security upgrades are on. - No analytics. The PostHog product analytics that are on by default upstream are switched off, and a same origin Content Security Policy stops the browser contacting any other site.
What is not included. Upstream Unstract also has a Docker socket "runner" that launches the File Classifier and Text Extractor container tools. The runner has root equivalent access to Docker, so this image 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.
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.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.xlarge (16 GiB)
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet.
- A security group that allows TCP 22 (SSH), TCP 443 (the web interface and API) and TCP 80 (redirects to HTTPS) from your own networks only.
- m5.xlarge (4 vCPU, 16 GiB) or larger. The stack and the two local models need 16 GiB of memory; on a smaller instance first boot writes a warning to the instance's console output, and extraction can run out of memory when the models load. A burstable t3.xlarge works for occasional, interactive use; turn on unlimited CPU credits, because continuous extraction on the CPU spends credits faster than a t3 earns them.
- A 64 GiB root volume (the default for this AMI).
- Optional: an API key or endpoint for a hosted model (Amazon Bedrock, OpenAI, Anthropic, Google Gemini, Mistral, Azure OpenAI and others) if you want faster or stronger extraction than the local models.
Be realistic about the bundled CPU models. They make the image useful, private and free of API keys from the first minute; they are not fast. They run on the instance's processor, not a GPU, so a one page document takes tens of seconds once the models are loaded, and the first extraction after a launch or a reboot takes longer while the models are read from the volume. Time grows with document length and the number of prompts, and scanned pages are slower than text PDFs because they go through OCR. For high volumes, connect a hosted model in Settings (Step 10); Unstract lets you choose the model per project.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Unstract 0.203.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.xlarge, your key pair and a security group as above.
With the AWS CLI (replace the values in angle brackets). IMDSv2 is required, and a hop limit of 1 keeps the metadata service out of reach of anything that is not the instance itself:
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.xlarge --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled,HttpPutResponseHopLimit=1 \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=unstract}]'
Unstract follows the instance's address: it refreshes on every boot, so after a stop and start without an Elastic IP the new public address and public DNS name are trusted for sign in automatically (the URL written in /root/unstract-credentials.txt stays the first boot one). An Elastic IP is still recommended, so the address you bookmark and point DNS at never changes. If the instance has no public IPv4 address, or the metadata service does not answer at boot, the last good address list is kept.
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Unstract 0.203.0 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Confirm the services are running
First boot takes several minutes: it generates this instance'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 unstract-docker-user ollama unstract-model-gateway unstract nginx
Expected output:
first boot complete
active
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.29.250.1 unless your VPC already uses that range, see Step 11), 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.29.250.1:11434
172.29.250.1:53
[::]:22
[::]:443
[::]:80
Step 4: Read this instance's login
sudo cat /root/unstract-credentials.txt
# Unstract - per-VM credentials generated on first boot (2026-11-09T10:00:00Z).
# Keep this file private. Sign in at UNSTRACT_URL. Change the password with: sudo unstract-set-password
UNSTRACT_URL=https://<public-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.29.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 instance. 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 instance is marked ready.
Step 5: Sign in
Open https://<public-ip>/ in your browser. The certificate is self signed for this instance, 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 4. You land in Prompt Studio.
Step 6: 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 instance's key. Without the key it refuses the request:
GW=$(sudo unstract-netplan get app_gw)
curl -s -o /dev/null -w '%{http_code}\n' http://$GW: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:
GW=$(sudo unstract-netplan get app_gw)
sudo sed -n 's/^LOCAL_MODEL_GATEWAY_KEY=\(.*\)/header = "Authorization: Bearer \1"/p' /root/unstract-credentials.txt \
| curl -s -K - http://$GW:11434/v1/models | python3 -m json.tool | grep '"id"'
"id": "nomic-embed-text:latest",
"id": "granite4:tiny-h",
Step 7: 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 -i <your-key.pem> ubuntu@<public-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. It takes a few minutes, and longer on the first run after launch while the models are read from the new volume:
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 (26 s)
Step 8: 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 instance, 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 <public-ip> with your instance's address and <API_KEY> with your key (-k is only needed while you use the self signed certificate):
curl -sk -X POST "https://<public-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 9: 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 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 (Amazon Bedrock, OpenAI, Anthropic, Google Gemini, Mistral, Azure OpenAI, 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 instance's own encryption key. The containers cannot use the instance's IAM role, because they are fenced off from the metadata service by design (that is what stops a crafted URL from stealing the instance's credentials). For Amazon Bedrock and S3, create a dedicated IAM user whose policy allows only what Unstract needs, for example bedrock:InvokeModel on the one model you use and s3:ListBucket, s3:GetObject and s3:PutObject on the one bucket, and give its access key to the adapter or connector. If that key ever leaks it reaches that model and that bucket, nothing else.
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 from LOCAL_MODEL_GATEWAY_URL in /root/unstract-credentials.txt, 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: 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 IPv4 address or IPv6 /64. 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.
Cross origin requests. nginx refuses a request that changes data (POST, PUT, PATCH, DELETE) or opens a WebSocket when its Origin header is not this instance's own address, before Django sees it. API clients such as curl and your applications send no Origin header and are not affected. nginx also serves only this instance's own names (its public and private addresses and DNS names, and any you add in Step 13): a request for any other host name is dropped. Both lists are refreshed from the metadata service on every boot and are never widened to accept any origin or any host.
Docker subnets. First boot reads your VPC's address blocks and places the Docker networks on the first range that overlaps none of them: 172.29.250.0/24 to 172.29.254.0/24, otherwise 10.255.250.0/24 upwards, otherwise 192.168.250.0/24 upwards, so every host in your VPC stays reachable. The gateway address in the credentials file follows that choice. If your VPC overlaps all three, first boot writes a message to the instance's console output and the stack does not start; launch it in a VPC that leaves one free. sudo unstract-netplan get prefix shows the choice.
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 | public internet addresses (hosted models, cloud storage, webhooks), the model gateway with the instance's key, the data services | the instance metadata service (169.254.169.254, fd00:ec2::254), link local and reserved ranges, your VPC's address blocks, the instance'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 |
The VPC address blocks are read from the metadata service by the host at every boot, so the fence follows the VPC the instance is in. You can see it from inside a container:
sudo docker exec unstract-backend python3 -c '
import urllib.request, urllib.error
for host in ["169.254.169.254", "[fd00:ec2::254]"]:
req = urllib.request.Request("http://" + host + "/latest/api/token", method="PUT",
headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"})
try:
urllib.request.urlopen(req, timeout=5); print(host, "REACHABLE")
except urllib.error.HTTPError as e:
print(host, "REACHABLE (HTTP %d)" % e.code)
except Exception:
print(host, "blocked")'
169.254.169.254 blocked
[fd00:ec2::254] 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 instance'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: Let the containers reach a private service
Because your VPC's address blocks are refused, a connector or model server inside your VPC (for example a PostgreSQL database or a self hosted model endpoint) is unreachable until you allow it. Add one address or CIDR per line to /etc/unstract/egress-allow.conf, then re-apply the guard; the stack keeps running:
sudo nano /etc/unstract/egress-allow.conf # add a line such as 10.0.12.34/32
sudo unstract-netguard-apply
Allow only the specific addresses you need. Metadata, link local, loopback and multicast ranges can never be allowed: unstract-netguard-apply refuses them.
Step 13: Use your own domain and a trusted certificate
- Point a DNS record for your domain at the instance's Elastic IP, and allow inbound
80from the internet while you request the certificate. - Get a certificate with certbot and install it where nginx reads it (replace
unstract.example.com). nginx is stopped for a few seconds while certbot answers the challenge, and the deploy hook installs each renewal too:
sudo apt-get install -y certbot
sudo certbot certonly --standalone -d unstract.example.com \
--pre-hook 'systemctl stop nginx' --post-hook 'systemctl start nginx' \
--deploy-hook 'install -m 0644 "$RENEWED_LINEAGE/fullchain.pem" /etc/unstract/tls/server.crt && install -m 0600 "$RENEWED_LINEAGE/privkey.pem" /etc/unstract/tls/server.key'
- Tell Unstract its new name, so sign in redirects, CSRF checks, the origin guard and the served host names all include your domain. Add it, one name per line, to
/etc/unstract/origins-custom.conf(the first name becomes Unstract's URL; the instance's own addresses stay trusted), then apply it to the running stack, which recreates the backend container and restarts nginx:
echo unstract.example.com | sudo tee /etc/unstract/origins-custom.conf
sudo unstract-origin-refresh --now
The names in that file are kept on every boot's address refresh. Do not edit the address keys in /etc/unstract/backend.env by hand: the refresh rewrites them at the next boot.
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 instance:
grep -E '^(unstract|ollama|chat_model|embed_model)=' /usr/share/unstract-appliance/VERSIONS | cut -c1-90
unstract=v0.203.0
ollama=v0.40.15 (sha256 628c018404a4206493efd38e46251c154ede5141b8b8dc85c659b3b027c47379)
chat_model=granite4:tiny-h (manifest sha256 566b725534ea0e9824f844abe6a78e1ab6f7357f1efb54
embed_model=nomic-embed-text:latest (manifest sha256 0a109f422b47e3a30ba2b10eca18548e944e8
Backups. Your projects, deployments, uploaded documents and vectors live in /var/lib/unstract (PostgreSQL, MinIO, Qdrant and Prompt Studio files) and the per instance secrets in /etc/unstract. Back up both together, with the stack stopped (sudo systemctl stop unstract), or take an EBS snapshot of the volume. 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 https://github.com/Zipstack/unstract at commit 86a63b848923baaff183280609bb5ca878471e63 (tag v0.203.0). The image runs the official images of that release with configuration in /opt/unstract and two changes on top: Python dependencies raised to fixed releases within Unstract's own version ranges, and the PDF viewer's eval feature test in the served web interface bundle switched off. The document parser is built from https://github.com/Unstructured-IO/unstructured-api at commit 14fedd55bc40cc5ec71fac3f3ad73a07feb2886b; 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 instance's Region, instance type and a description of the issue.