Applications Azure

Open Archiver on Ubuntu 24.04 on Azure User Guide

| Product: Open Archiver on Ubuntu 24.04 LTS on Azure

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 on 127.0.0.1:4000
  • open-archiver-frontend.service — the web console on 127.0.0.1:3000
  • open-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.service reverse proxy on TCP 80, TLS ready
  • open-archiver-firstboot.service generating every per instance secret and creating the administrator on first boot
  • A dedicated 32 GiB data volume mounted at /var/data/open-archiver for 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

All nine Open Archiver units reported active by systemctl, plus the firstboot service, on a freshly deployed instance

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

The per instance Open Archiver credentials file with the administrator, database, search and queue secrets masked, and the file confirmed as mode 600 owned by root

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

The Open Archiver health endpoint returning needsSetup false, the dedicated 32 GiB archive volume mounted at /var/data/open-archiver, and the listening sockets showing only SSH and nginx on public addresses

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:

The Open Archiver sign in page served over port 80, asking for the administrator e-mail and password

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:

The Open Archiver dashboard showing total emails archived, total storage used, failed ingestions, an ingestion history chart and storage by ingestion source

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:

The Open Archiver archived emails list showing archived messages with their dates, subjects, senders, mailbox and original folder path, each with a View action

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 Open Archiver search page showing a full text query with three matching results, the query term highlighted in each subject and message body, and the query time reported

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 Open Archiver AGPL-3.0 source offer shipped on the instance, and the shipped source tarball verifying against its recorded SHA-256 checksum

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 to 2 to fit a two vCPU instance. Raise it on a larger VM.
  • ENABLE_DELETIONfalse in this image, so nothing in the archive can be deleted through the console. Set it to true only if your retention policy genuinely requires deletion.
  • STORAGE_TYPElocal in this image. Set it to s3 with the STORAGE_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.