Open Archiver on Ubuntu 24.04 on Azure User Guide
Overview
Open Archiver is an open source, self hosted email archiving platform built for organisations that must keep a complete, searchable and legally defensible record of their email. It connects to Google Workspace, Microsoft 365 or any IMAP server and continuously archives entire mailboxes, and it can also import existing PST, MBOX and EML files. Every message and attachment is stored immutably, its full text is extracted and indexed, and a searchable console lets compliance, legal and IT teams find and export exactly what they need.
The cloudimg image installs Open Archiver 0.5.1 natively and assembles the whole stack for you: the Node.js API and web console, three background workers for ingestion, indexing and scheduled sync, PostgreSQL 16 for metadata, Meilisearch 1.49 for full text search and Redis 7 for the job queues, all behind an nginx reverse proxy on port 80. Archived messages, attachments and the search index live on a dedicated data volume mounted at /var/data/open-archiver, so the archive grows on its own disk and can be expanded or snapshotted independently of the operating system.
The appliance is secure by default, and unusually strongly so: Open Archiver creates its first administrator through a one time setup call that the API accepts only while no users exist, which means this image genuinely ships with no application login at all rather than a vendor default to rotate. On the first boot of every instance a one shot service generates a unique database password, search engine master key, queue password, token signing secret, storage encryption key and administrator password, creates the administrator, then proves the new credentials authenticate and that the setup endpoint is closed. Every datastore is bound to the loopback interface, so only ports 22 and 80 need to be reachable.
What is included:
- Open Archiver 0.5.1 built from the pinned upstream release tag (
/opt/open-archiver/src) open-archiver.service— the Node.js API on127.0.0.1:4000open-archiver-frontend.service— the web console on127.0.0.1:3000open-archiver-ingestion.service,open-archiver-indexing.service,open-archiver-scheduler.service— the three background workers- PostgreSQL 16 for archive metadata, Meilisearch 1.49 for full text search, Redis 7 for job queues, all on loopback
nginx.servicereverse proxy on TCP 80, TLS readyopen-archiver-firstboot.servicegenerating every per instance secret and creating the administrator on first boot- A dedicated 32 GiB data volume mounted at
/var/data/open-archiverfor messages, attachments and the search index - The complete AGPL-3.0 corresponding source shipped on the instance
- Ubuntu 24.04 LTS base, latest patches, unattended security upgrades enabled
- 24/7 cloudimg support, 24h response SLA
Prerequisites
An active Azure subscription, an SSH key, and a VNet with a subnet. Recommended VM size: Standard_B2s (2 vCPU, 4 GB RAM). The whole nine service stack is memory tuned to run comfortably in 4 GB with no swap; move to a larger size only if you archive very large mailboxes concurrently.
Step 1: Deploy from the Azure Portal
Search the Azure Marketplace for Open Archiver, choose the plan, and create the VM. In the networking step attach an NSG that allows inbound TCP 22 (SSH) and TCP 80 (the console) from your client networks only. Put a TLS reverse proxy in front of port 80 for production, since archived mail is sensitive by definition. No other port needs to be open: PostgreSQL, Redis, Meilisearch and the web console itself all listen on the loopback interface, and the API's port 4000 is reachable only from the VM and whatever your NSG permits, so leave it closed.
Step 2: Deploy from the Azure CLI
RG="open-archiver-prod"; LOCATION="eastus"; VM_NAME="open-archiver-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/open-archiver-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az network vnet create -g "$RG" --name oa-vnet --address-prefix 10.112.0.0/16 --subnet-name oa-subnet --subnet-prefix 10.112.1.0/24
az network nsg create -g "$RG" --name oa-nsg
az network nsg rule create -g "$RG" --nsg-name oa-nsg --name allow-ssh --priority 100 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name oa-nsg --name allow-web --priority 110 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 80 --access Allow --protocol Tcp
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
--size Standard_B2s --storage-sku StandardSSD_LRS \
--admin-username azureuser --ssh-key-values "$SSH_KEY" \
--vnet-name oa-vnet --subnet oa-subnet --nsg oa-nsg --public-ip-sku Standard
The archive data disk is part of the image, so it is created and mounted for you automatically. There is no extra disk to attach.
Step 3: Connect via SSH
ssh azureuser@<vm-ip>
All nine units start automatically on first boot, after open-archiver-firstboot.service has generated this instance's secrets and created the administrator. First boot typically completes within a minute of the VM reporting ready.
Step 4: Verify the Services
sudo systemctl is-active postgresql redis-server meilisearch open-archiver open-archiver-frontend open-archiver-ingestion open-archiver-indexing open-archiver-scheduler nginx
sudo test -f /var/lib/cloudimg/open-archiver-firstboot.done && echo FIRSTBOOT_DONE
sudo ss -tlnH | awk '{print $1, $4}' | sort -u
Expected output — all nine units are active, first boot has completed, and only SSH and nginx are on a public address while the console, database, cache and search engine stay on loopback:
active
active
active
active
active
active
active
active
active
FIRSTBOOT_DONE
LISTEN *:4000
LISTEN 0.0.0.0:22
LISTEN 0.0.0.0:80
LISTEN 127.0.0.1:3000
LISTEN 127.0.0.1:5432
LISTEN 127.0.0.1:6379
LISTEN 127.0.0.1:7700

Step 5: Retrieve Your Administrator Credentials
Every secret on this instance is generated on its own first boot and written to a root only file. Read it, then store the values somewhere safe:
sudo cat /root/open-archiver-credentials.txt
sudo stat -c '%a %U:%G %n' /root/open-archiver-credentials.txt
The file holds the console URL, the administrator e-mail and password, and the PostgreSQL, Meilisearch and Redis credentials. Nothing here is shared with any other deployment:
open-archiver.url=http://<vm-ip>/
open-archiver.admin.email=admin@openarchiver.local
open-archiver.admin.pass=<unique-per-instance-password>
open-archiver.db.name=open_archive
open-archiver.db.user=openarchiver
open-archiver.db.pass=<unique-per-instance-password>
open-archiver.meili.key=<unique-per-instance-key>
open-archiver.redis.pass=<unique-per-instance-password>
600 root:root /root/open-archiver-credentials.txt

Step 6: Confirm There Is No Default Login
Open Archiver has no built in administrator account: the first one is created through a one time setup call that the API refuses once any user exists. This image performs that call on first boot with a password unique to your instance, so the setup screen can never be claimed by a passing visitor. Prove all of it — the console is up, setup is already complete, your per instance password authenticates, and a guessable password does not:
ADMIN_EMAIL=$(sudo grep '^open-archiver.admin.email=' /root/open-archiver-credentials.txt | cut -d= -f2-)
curl -s -m 15 http://127.0.0.1/api/v1/auth/status; echo
curl -s -o /dev/null -w 'per-instance password -> HTTP %{http_code}\n' -m 20 -X POST http://127.0.0.1/api/v1/auth/login \
-H 'Content-Type: application/json' -d "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"<ADMIN_PASSWORD>\"}"
curl -s -o /dev/null -w 'guessable password -> HTTP %{http_code}\n' -m 20 -X POST http://127.0.0.1/api/v1/auth/login \
-H 'Content-Type: application/json' -d "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"securepassword123\"}"
curl -s -o /dev/null -w 'setup endpoint -> HTTP %{http_code}\n' -m 20 -X POST http://127.0.0.1/api/v1/auth/setup \
-H 'Content-Type: application/json' -d '{"email":"intruder@example.com","password":"Zt7pQ2rL9xVn4Kd1BwYs","first_name":"x","last_name":"y"}'
Setup reports complete, the per instance password is accepted, the guessable password is rejected, and the one time setup endpoint is closed with a 403:
{"needsSetup":false}
per-instance password -> HTTP 200
guessable password -> HTTP 401
setup endpoint -> HTTP 403
A helper on the instance runs this same proof across every built in account — the administrator, the PostgreSQL role, the Meilisearch master key and the Redis password — and confirms each upstream default is rejected:
sudo bash /usr/local/sbin/open-archiver-cred-roundtrip.sh
roundtrip: the per-VM administrator, database, search and queue credentials all authenticate, and every upstream default was rejected
Step 7: Check the Console and the Archive Volume
curl -s -o /dev/null -w 'console -> HTTP %{http_code}\n' -m 15 -L http://127.0.0.1/
df -h /var/data/open-archiver | tail -2
findmnt -no SOURCE,FSTYPE,SIZE,TARGET /var/data/open-archiver
The console answers on port 80, and the dedicated archive volume is mounted as its own filesystem:
console -> HTTP 200
Filesystem Size Used Avail Use% Mounted on
/dev/sdc 32G 496K 30G 1% /var/data/open-archiver
/dev/sdc ext4 31.2G /var/data/open-archiver

Archived messages and attachments are written under /var/data/open-archiver/archive, and the Meilisearch index lives in /var/data/open-archiver/meili. To grow the archive later, expand the data disk in Azure and then extend the filesystem with sudo resize2fs /dev/sdc.
Step 8: Sign in to the Console
Browse to http://<vm-ip>/ and sign in with the administrator e-mail and password from Step 5. Any request to the console root redirects to the sign in page until you are authenticated:

Change the administrator e-mail address and password to your own under Settings > Users as soon as you have signed in, and add accounts for the rest of your compliance and legal team there.
Step 9: The Dashboard
The dashboard summarises the archive: how many messages are held, how much storage they occupy, how many ingestions have failed recently, an ingestion history chart and a storage breakdown per source. On a new instance the counters start at zero and fill in as your first mailbox is archived:

Step 10: Connect a Mailbox
Open Archive > Ingestion Sources and choose Create New. Open Archiver supports:
- Google Workspace — upload a service account key with domain wide delegation and give the super admin address to impersonate, and it archives every mailbox in the domain.
- Microsoft 365 — register an application in Entra ID and supply the client ID, client secret and tenant ID.
- Generic IMAP — host, port, TLS setting, username and password for a single mailbox or a shared archive account.
- PST, MBOX and EML import — upload an export, or point the importer at a file already on the instance under
/var/data/open-archiver/archive/temp/for very large archives.
Once a source is saved, the ingestion worker begins archiving immediately and the sync scheduler keeps it current on the schedule set by SYNC_FREQUENCY (every 15 minutes in this image). The indexing worker extracts and indexes the full text of each message as it lands, so new mail becomes searchable within seconds.
Archived mail appears under Archive > Archived Emails, with the original folder structure, sender, recipient and date preserved, and every message viewable in place:

Step 11: Search the Archive
Search runs a full text query across every archived message and attachment through Meilisearch, with fuzzy matching and highlighted hits, and reports how long the query took. This is what makes the archive useful for an audit or an eDiscovery request:

The same search is available over the API for scripted exports. Sign in for a token, then query:
ADMIN_EMAIL=$(sudo grep '^open-archiver.admin.email=' /root/open-archiver-credentials.txt | cut -d= -f2-)
TOKEN=$(curl -s -m 20 -X POST http://127.0.0.1/api/v1/auth/login -H 'Content-Type: application/json' \
-d "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"<ADMIN_PASSWORD>\"}" \
| sed -n 's/.*"accessToken"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
curl -s -m 20 "http://127.0.0.1/api/v1/search?keywords=invoice&page=1&limit=5" -H "Authorization: Bearer $TOKEN"; echo
curl -s -m 20 http://127.0.0.1/api/v1/dashboard/stats -H "Authorization: Bearer $TOKEN"; echo
The search response carries the matching messages plus Meilisearch's own query time, and the dashboard endpoint reports the archive totals. On a brand new instance with nothing archived yet the result set is legitimately empty:
{"hits":[],"total":0,"page":1,"limit":5,"totalPages":0,"processingTimeMs":6}
{"totalEmailsArchived":0,"totalStorageUsed":0,"failedIngestionsLast7Days":0}
Step 12: Server Components
node -v
node -e "console.log('Open Archiver '+require('/opt/open-archiver/src/package.json').version)"
/usr/local/bin/meilisearch --version
psql --version | awk '{print $1, $2, $3}'
redis-server --version | sed -E 's/^(Redis server v=[0-9.]+).*/\1/'
free -h | sed -n '1,2p'
The pinned runtime and datastore versions, and the memory headroom with the entire nine service stack running:
v22.23.1
Open Archiver 0.5.1
meilisearch 1.49.0
psql (PostgreSQL) 16.14
Redis server v=7.0.15
total used free shared buff/cache available
Mem: 3.8Gi 1.2Gi 1.7Gi 34Mi 1.3Gi 2.6Gi
Step 13: The AGPL-3.0 Source Offer
Open Archiver is licensed under the GNU Affero General Public License version 3. Section 13 of that licence requires anyone who interacts with the program over a network to be offered its corresponding source. This instance satisfies that on disk, with no network access required:
ls -1 /usr/share/open-archiver/source/ /usr/share/doc/open-archiver/
cd /usr/share/open-archiver/source && sha256sum -c open-archiver-0.5.1-source.tar.gz.sha256
The full licence text, the written offer, and a checksum verified tarball of exactly the source tree that produced the running binaries:
/usr/share/doc/open-archiver/:
LICENSE
SOURCE-OFFER.txt
/usr/share/open-archiver/source/:
open-archiver-0.5.1-source.tar.gz
open-archiver-0.5.1-source.tar.gz.sha256
open-archiver-0.5.1-source.tar.gz: OK

The live source tree is at /opt/open-archiver/src, and /usr/share/doc/open-archiver/SOURCE-OFFER.txt states the offer in full. cloudimg makes no modifications to Open Archiver itself; this image adds only configuration, systemd units, an nginx vhost and the first boot credential bootstrap. If you redistribute this image or give others network access to it, pass the source offer on.
Step 14: Configuration and Logs
The application environment lives in /etc/open-archiver/open-archiver.env (mode 0640, root owned) and the Meilisearch environment in /etc/open-archiver/meilisearch.env. Both are written by the first boot service and hold this instance's secrets. After editing either one, restart the affected units:
sudo grep -E '^(NODE_ENV|PORT_BACKEND|PORT_FRONTEND|STORAGE_TYPE|STORAGE_LOCAL_ROOT_PATH|SYNC_FREQUENCY|INGESTION_WORKER_CONCURRENCY|ENABLE_DELETION)=' /etc/open-archiver/open-archiver.env
sudo journalctl -u open-archiver -n 20 --no-pager
Useful settings you may want to change:
SYNC_FREQUENCY— the cron expression controlling incremental mailbox sync,*/15 * * * *in this image.INGESTION_WORKER_CONCURRENCY— how many mailboxes are archived in parallel, set to2to fit a two vCPU instance. Raise it on a larger VM.ENABLE_DELETION—falsein this image, so nothing in the archive can be deleted through the console. Set it totrueonly if your retention policy genuinely requires deletion.STORAGE_TYPE—localin this image. Set it tos3with theSTORAGE_S3_*variables to keep message bodies in object storage instead of on the data volume.
To serve the console over HTTPS, point a DNS name at the VM and add a TLS certificate to the nginx vhost at /etc/nginx/sites-available/cloudimg-open-archiver. The console derives its own public address from the proxy headers nginx sets, so it works on any hostname with no further configuration.
Troubleshooting
The console returns 502. The web console did not start or is still starting. Check sudo systemctl status open-archiver-frontend and sudo journalctl -u open-archiver-frontend -n 50. The nginx vhost ships with enlarged proxy buffers because an authenticated console response is larger than nginx's default 4 KB header buffer; if you replace the vhost, keep the proxy_buffer_size, proxy_buffers and large_client_header_buffers directives.
The console will not accept the administrator password. Read it again with sudo cat /root/open-archiver-credentials.txt; it is unique to this instance and is never the value in any documentation. If the file still contains only the placeholder comment, first boot has not finished — check sudo systemctl status open-archiver-firstboot.
A mailbox is not being archived. Check the ingestion worker with sudo journalctl -u open-archiver-ingestion -n 100, then open the source under Archive > Ingestion Sources to see its last sync status. Authentication failures against Google Workspace or Microsoft 365 almost always mean the service account or application registration lacks mailbox wide permission.
Messages are archived but not searchable. The indexing worker feeds Meilisearch separately from ingestion. Check sudo journalctl -u open-archiver-indexing -n 100 and confirm Meilisearch is healthy under Admin, which reports the engine version, health and index document count. A reindex can be triggered from that same page.
The archive volume is filling up. Expand the data disk in the Azure portal, then run sudo resize2fs /dev/sdc. Nothing needs to be stopped. For very large archives, consider switching STORAGE_TYPE to s3.
Support
24/7 support from cloudimg is included with this image. Email support@cloudimg.co.uk for help with deployment, connecting Google Workspace, Microsoft 365 or IMAP sources, importing existing PST and MBOX archives, search indexing, retention and export workflows, TLS setup, version upgrades, performance tuning, or PostgreSQL, Meilisearch and Redis administration. Critical issues receive a one hour average response.