Skip to content

Deployment overview

archAIc ships as a set of Docker images. You place your PrIx corpus on disk, set a few variables in .env, and bring the stack up with docker compose — the lexical and semantic indexes build themselves on first start. The same flow works whether you self-host in your own server room or run on a cloud VM.

This page covers the quickstart, the two deployment models, the docker-compose surface, and the environment variables you typically set in .env. For the full step-by-step — corpus layout, every .env variable, and the first-run build — see Deploy a collection. The companion page LLM setup covers the optional Ollama install.

A node comes up in three steps; the lexical and semantic indexes build themselves on first start. The Deploy a collection walkthrough has the detail — corpus layout, every .env variable, and the build timings.

Terminal window
# 1. Configure the deployment.
cp .env.example .env
${EDITOR:-nano} .env # set CORPORA_PATH, APP_NAME, GD_BUILD_INDEX=1
# 2. Place your PrIx corpus under the CORPORA_PATH you just set.
# 3. Start the stack with the profile you want.
docker compose --profile rust up -d
docker compose logs -f rust-engine # follow the one-time index build

On first start — with GD_BUILD_INDEX=1 — the engine ingests the PrIx output, builds the LMDB lexical index, then computes the semantic vectors into Qdrant. It does not serve queries until that finishes; docker compose ps shows rust-engine healthy when it is ready. Set GD_BUILD_INDEX=0 for every start after the first.

Docker compose maps the engine to host port ${RUST_PORT:-3030} — the 3030 the smoke-test below uses.

archAIc deployments fall into two shapes. Pick one based on customer requirements.

The default. Everything runs on a box owned and operated by the customer, in their own server room or rack. Recommended for national archives, large municipal repositories, dioceses, and any institution with sovereignty or GDPR-sensitive holdings.

A 16 GB box comfortably serves a 350K-page collection without an LLM. The figures below are the working-set RAM each service actually needs, measured on a production deployment whose semantic index covers ~348K pages (~7M passages). Qdrant and the kernel will then use any extra RAM you give them as page cache — desirable for query latency, but optional; the kernel reclaims it under pressure.

ServiceWorking setNotes
Rust engine~0.3–1 GB~300 MB at startup, ~1 GB steady while serving a loaded corpus. The only service that scales with concurrent traffic, not corpus size — it reached ~16 GB once in a stress test of 128 concurrent callers at 10K QPS, far past any real archive’s load.
Qdrant~5 GBThe vector store and the service that scales with the corpus — dense and sparse vectors for every page, passage, and line. ~5 GB heap for the ~348K-page / ~7M-passage index, matching the spec sizing of ~1 GB per million passages (the lines collection is mmap’d on disk, which is what keeps RAM bounded). On a roomy host, Qdrant happily uses extra RAM as mmap page cache — a long-running production container has been seen at ~20 GB resident, but /proc/<pid>/status shows ~9 GB of that is reclaimable file-backed cache. The hard floor tracks the heap, not the resident set.
Nominatim~2 GBOptional — the geocoder behind IIIF navPlace. Flat working set; varies with the OSM extract you load.
Grafana~140 MBOptional. Grafana alone; the full observability profile (Grafana + Prometheus + Loki + Tempo + Promtail) is ~900 MB combined.
LLM (Ollama)model-dependentOptional, and the heaviest component when present. A 32B-class model wants tens of GB of RAM or VRAM; a small model far less. Size the box separately for whichever model you choose.

Total engine-tier working set for the ~348K-page deployment above — engine + Qdrant + Nominatim, no LLM — is about 8 GB. A 16 GB box leaves the other half as headroom that Qdrant fills with mmap page cache for free query-latency wins; corpora can grow well past 350K pages before that becomes tight (Qdrant scales linearly at ~1 GB per million additional passages). The LLM tier sits on top of these numbers — size separately for the model you pick.

ResourceRecommendedNotes
CPU4 coresMore cores speed up the one-time ingest; the query path is I/O-bound.
Disk3× corpus size, SSDOriginal images + LMDB lexical index + Qdrant collection + cfg/audit DB + Docker images. A ~17 GB image set lands around 50 GB on disk.
GPUOptionalSpeeds up the one-time semantic build (embedding generation). Not used at query time.
Swap16 GB, SSDCushions the Qdrant memory bump during the initial semantic build.

A local LLM is the component that drives big hardware. If you run Ollama on the same host for natural-language Q&A and curator autofill, the canonical box is a GB10-class mini-workstation (Nvidia DGX Spark, ASUS Ascent GX10, Acer Veriton GN100 — around 1 petaFLOP and 128 GB unified memory), everything co-resident. Without an LLM, the engine tier is far lighter — see the per-service table above. LLM setup covers sizing the model.

Managed in a cloud of customer’s choosing

Section titled “Managed in a cloud of customer’s choosing”

Same software, same docker-compose, hosted on a VM you provision in AWS, GCP, Azure, Hetzner, OVH, Scaleway, or any provider. The customer keeps the cloud account; archAIc operates the node. Hardware tier maps cleanly: a c6i.4xlarge (or n2-standard-16, or AX102) covers the typical-corpus envelope for engine-only deployments; add a GPU instance (or a separate GPU VM on a customer VLAN) for the LLM tier.

The trade-off is the same as anywhere else: cloud is faster to provision and easier to scale up; self-hosted is cheaper at steady-state and keeps data physically inside the customer’s perimeter. archAIc supports either with no code differences.

The docker-compose.yml exposes profiles so you start only what you need.

ServiceImage sourcePort (internal)ProfileNotes
phpdocker/Dockerfile.php80rust, cpp, bothCurator and researcher web UI. Today: Apache + PHP 8 (the dipucoru deployment). The in-progress UI branch (Twig + Pest + Lit + FrankenPHP) runs side-by-side as newdipucoru.
rust-enginedocker/Dockerfile.rust3030rust, bothThe REST API. The default backend for all new deployments.
cpp-enginedocker/Dockerfile.cpp7010cpp, bothLegacy C++ KWS engine. Search-only; no semantic, DLU, sync, GDPR. Kept for read-compatibility with older deployments.
mcp-serverdocker/Dockerfile.mcp3032rust, bothMCP server for AI agents. OAuth 2.1, Streamable HTTP.
qdrantqdrant/qdrant:v1.176333 / 6334rust, bothVector store for semantic search.
cantaloupeuclalibrary/cantaloupe:5.0.7-08182iiif (optional)Optional IIIF Image API tile server. Use when you want pyramid TIFF tiles instead of archAIc’s built-in JPEG serving.
nominatimmediagis/nominatim:4.48080rust, bothGeocoder for navPlace. Resolves place names to WGS84 points for the IIIF navPlace FeatureCollection.
miradorupstream imagen/aiiif (optional)Bundled Mirador IIIF viewer.
grafana / prometheus / tempo / loki / promtailupstream imagesvariousobservabilityOpenTelemetry pipeline. Run alongside rust for metrics, logs, traces.

Ollama is not in the canonical docker-compose.yml; operators add it themselves. See LLM setup for the sidecar snippet.

The reverse proxy (nginx or Apache, customer-provided) exposes only the PHP UI on port 443 and optionally the MCP server on its own subdomain. Engine, Qdrant, and any operator-added Ollama ports are bound to 127.0.0.1 and not user-facing.

The .env file is the primary deploy-time configuration. The full reference lives in .env.example; the variables you almost always touch:

Terminal window
# Identity and paths
APP_NAME=rmcr # public URL slug
CORPORA_PATH=/srv/archaic/corpora # host path to your corpus root
# Ports (host side; container side is fixed)
PHP_PORT=6008
RUST_PORT=3030
MCP_PORT=3032
# Engine selection
KWS_BACKEND_URL=http://rust-engine:3030 # use rust-engine for new deployments
# Lexical build
GD_BUILD_INDEX=1 # set on first run; flip to 0 after
GD_INDEX_DIR=/corpora/gd-index
GD_DB_PATH=/corpora/corpora.db
# Semantic
QDRANT_URL=http://qdrant:6334
GD_EMBED_MODEL=/models # path mounted from EMBED_MODEL_PATH
GD_EMBED_BATCH_SIZE=64
GD_EMBED_THREADS=0 # 0 = autodetect
# Optional LLM
OLLAMA_URL=http://ollama:11434
OLLAMA_MODEL=qwen3:32b
OLLAMA_AUTH= # bearer token if you front Ollama with a proxy
# API keys
GD_API_KEYS= # CSV of bearer keys for the REST API
GD_MCP_API_KEY= # MCP service-level key
GD_SECRET_KEY= # libsodium key for encrypted cfg rows
Terminal window
curl -fsS http://localhost:3030/health # engine health
curl -fsS "http://localhost:3030/v1/query?q=archive&limit=5" | head -c 500 # KWS
curl -fsS http://localhost:3030/v1/index-stats # confirms semantic passages
curl -fsS http://localhost:6008/${APP_NAME}/health # PHP UI

All five should return cleanly. If index-stats reports zero passages, Qdrant is unreachable or the embedding pass has not run. Check docker compose logs -f rust-engine.

  • Feature matrix. What’s core vs optional, and what each optional layer needs in software and hardware.
  • LLM setup. Installing Ollama and pointing archAIc at it.
  • Architecture overview. The layered shape underneath the compose surface.