Export as an appliance
Requires the Pro plan or above.
Some projects can’t run on a shared cloud at all. A hospital system whose data isn’t allowed to leave its own network. A government contract that requires on-prem delivery. A reseller shipping the same product into a dozen customer environments that must never depend on each other. For those, “run it in our cloud, just with extra isolation” isn’t good enough — the whole system has to run somewhere the customer controls.
Appliance export is how you deliver that: a single sealed, signed archive that carries the application, its agents, the governed data model and the policies that enforce it, plus a runtime to run all of it. You install it with Docker Compose on infrastructure you or your customer owns. Once it’s up, it never calls qRaptor again to serve traffic — qRaptor is only contacted to pull the images at install or upgrade time.
Early access. Appliance export has been installed and served off-platform, but it is not yet generally available. Air-gapped install doesn’t exist yet, and the supported install target is Docker Compose on linux/amd64 only. Read What is not supported yet before you plan a delivery around it.
Is this what you need?
qRaptor gives you three ways to put a project in front of users, and they solve different problems:
| Where it runs | Use it when | |
|---|---|---|
| Managed cloud | qRaptor’s infrastructure | The default. No constraint on where data or compute lives. |
| Remote executors | Mostly qRaptor, with specific steps running inside your network | An agent’s reasoning and orchestration can stay on the managed cloud, but one step — a query, a tool call — has to touch a private database or internal API. |
| Appliance export (this page) | Entirely on infrastructure you control | The whole system — app, agents, data, policies — has to run with no dependency on qRaptor’s cloud at all, including after it’s installed. |
If only a piece of an agent’s work needs to stay inside your network, use a remote executor — it’s far less to operate than a whole appliance. Reach for an appliance when nothing can be managed centrally: no shared database, no shared runtime, no ongoing connection.
Prerequisites
- Admin role.
- A project with at least one agent, app or multi-agent system published to production — an export carries what’s live, not work in progress.
- A
linux/amd64host with Docker and Docker Compose v2, for installing it.
What you get
One file — appliance-<project>-<version>.tar.gz — that unpacks to a directory containing:
compose.yaml | The full service topology, every image pinned by digest |
bundle/ | The sealed configuration: app definition, roles, permissions, page access, data governance (tables, columns, row filters), pinned agent and multi-agent-system versions, schedules, Keycloak realm, project configs, LLM configuration and runtime config |
manifest.yaml + manifest.sig | A SHA-256 of every sealed file, and a detached Ed25519 signature over it |
install.sh, verify.sh | Verify the archive, then pull by digest and start |
.env.example | Every value you need to supply, starting with your public URL |
db-init/, seed/, otel/ | Database schema, Keycloak realm seeding, and an optional telemetry collector |
README.md | Install instructions generated for this specific export, including every schedule it will start running and its cadence |
The archive is deterministic: the same project at the same version produces a byte-identical archive, and the download page shows its SHA-256.
Page-level access control, role and permission checks, and data governance (row filters and column masks) are all enforced inside the appliance exactly as they are on the managed cloud — the rules travel in the bundle, and the same services that enforce them in Studio enforce them here.
No secrets travel in the bundle. LLM keys, database passwords and other credentials are referenced by environment-variable name only — you supply the values at install time. Every export runs a secret scan, and a finding fails the export rather than shipping.
Exporting a project
Open the Appliance page
Open the project and go to Appliance. It lists every previous export with its version and status, and a button to start a new one.
Choose what to include
The wizard walks you through what travels in the archive: which agents, multi-agent systems and knowledge bases to include, how much data (schema only, or schema and rows), and whether to bundle telemetry.
Run a dry run first
Before building anything, run a dry run. It does the same project read and coherence check as a real export — missing agents, empty governed tables, an LLM route with no credential — and reports problems without building an image or consuming a version number.
Run the real export
Once the dry run is clean, export for real. It runs seven stages: read project state, check coherence, build the app’s two images, write the sealed bundle, pin every image digest, scan for secrets and sign, then archive.
Installing it
Get the release key and the archive
Download the archive from the export’s page. Get qRaptor’s release public key from the Appliance page too — never from inside the archive itself, since a tampered archive could ship its own matching fake key.
Mint a pull credential
From the export’s page, mint a registry pull credential. It’s short-lived, scoped to this project, and shown once — mint it right before you install, not in advance.
Fill in .env
Unpack the archive and copy .env.example to .env. See Filling in .env below for what every value is for.
If QR_PUBLIC_URL is a hostname rather than a bare IP, make sure that hostname resolves back to the appliance itself from inside its own containers, not just from your browser. Login exchanges a code with the identity service in a server-to-server step that uses the same public address as the browser, and most cloud hosts won’t let a container call back out to its own public IP or DNS name. Add an extra_hosts entry for that hostname pointing at host-gateway in a docker-compose.override.yml alongside compose.yaml to fix it — this isn’t automated yet, so you repeat it on every reinstall until it is.
Verify, then install
./verify.sh /path/to/qraptor-release.pub
./install.sh /path/to/qraptor-release.pubBoth commands take the release public key you downloaded in step 1 as their argument — not the pull credential. install.sh runs the same verification as verify.sh, then logs in to the registry with your pull credential (read from the QR_REGISTRY_USERNAME / QR_REGISTRY_PASSWORD environment variables, or typed in when it prompts), pulls every image by digest, checks image signatures if you have cosign installed, and starts the project. The gateway is the only published port.
On macOS, the default /usr/bin/openssl is LibreSSL and cannot verify Ed25519 signatures. verify.sh detects this and tells you to install OpenSSL 3.x.
The keys and credentials involved
Three different things gate an install, and mixing them up is the most common setup mistake:
| What it proves | Where you get it | Lifespan | |
|---|---|---|---|
| Release public key | This archive was genuinely sealed by qRaptor and hasn’t been altered since. Passed to verify.sh and install.sh as an argument. | The Appliance page in Studio. Always the same key across all your exports. | Doesn’t expire. Replaced only if qRaptor rotates it. |
| Registry pull credential | Lets Docker download this project’s images from qRaptor’s registry — proves nothing about the archive’s integrity. | Minted on demand from the export’s page. | 3 days. Mint it right before installing, not in advance. |
| cosign image signature (optional) | Each individual container image, not the archive as a whole, was built and published by qRaptor. Checked automatically if cosign is on your PATH. | Nothing to obtain — install.sh checks it using public key material shipped with the images themselves. | N/A |
The release key is the one that matters for trust: it’s what tells you the archive wasn’t tampered with in transit. The pull credential only controls whether you can download the images — a leaked pull credential lets someone pull your images, but it can’t forge a signature, so it can’t make a tampered archive pass verify.sh.
The release public key does double duty. You pass it as a command-line argument to verify.sh and install.sh once, to check the archive before you trust it. You also paste it into .env as QR_RELEASE_PUBLIC_KEY_FILE, so the running appliance re-checks its own signature every time it boots — not installing from a bad archive is one thing, refusing to keep running from one is another.
Filling in .env
Copy .env.example to .env and work through it in this order:
Identity — already filled in, leave alone. QR_SUBSCRIPTION_ID, QR_PROJECT_ID, QR_WORKSPACE_ID, QR_APP_SLUG, QR_REALM, QR_APP_DATA_SCHEMA, QR_PLAN_KEY and QR_DEPLOYMENT_ID identify this export and must match the signed bundle. Changing one doesn’t re-target the appliance — it breaks it.
Public address. QR_PUBLIC_URL is the address browsers will use — Keycloak’s issuer, every OIDC redirect and the app’s cookies all derive from it, and a wrong value fails login with no error in any log. QR_COOKIE_SECURE and QR_COOKIE_SAMESITE depend on whether a TLS terminator sits in front of the gateway (see the table below); QR_COOKIE_DOMAIN stays blank unless you’re serving from a subdomain pattern.
| Value | Set it to |
|---|---|
| Plain HTTP, bare IP or hostname | QR_COOKIE_SECURE=false, QR_COOKIE_SAMESITE=Lax |
| HTTPS, behind a TLS terminator | QR_COOKIE_SECURE=true, QR_COOKIE_SAMESITE=None |
QR_COOKIE_SAMESITE is case-sensitive — it must be Lax or None, capitalized exactly like that. A lowercase lax crashes the cookie writer on every login attempt.
Database. QR_DB_NAME, QR_DB_USER and QR_DB_PASSWORD — generate a fresh password for this appliance, never reuse one from elsewhere.
Login (Keycloak). KC_BOOTSTRAP_ADMIN / KC_BOOTSTRAP_ADMIN_PASSWORD create the one-time admin account used to seed the realm on first boot. KEYCLOAK_APPS_CLIENT_SECRET, KEYCLOAK_APPS_IDENTITY_SVC_SECRET and KEYCLOAK_APP_BACKEND_CLIENT_SECRET are internal service-to-service secrets — generate a long random value for each; they’re never typed in by a person. APP_ADMIN_INITIAL_PASSWORD is the first-login password for the app’s own admin user.
Signing. QR_RELEASE_PUBLIC_KEY_FILE points at the same release public key file you used with verify.sh — see The keys and credentials involved above. Leave it blank only for a throwaway test install; the appliance then boots with signature verification off, which it will tell you loudly rather than hide.
Service-to-service. DB_PROXY_HMAC_SECRET and QRUNX_JWT_SECRET are internal signing secrets. Generate a random 32-byte-or-longer value for each — any random-string generator is fine, there’s nothing project-specific about the value itself.
File storage. QR_DMS_ENABLED turns document upload/download on or off. QR_DMS_MAX_UPLOAD_MB caps a single upload — it protects the install host’s disk, it isn’t a plan limit.
Model and embedding credentials. One block per provider your project actually uses, named QR_CFG_<PROVIDER>_<CONFIG>_<FIELD> — for example an AWS Bedrock access key or a Google service-account JSON. The export only emits the ones your project’s configurations reference; see Connecting models above for what each provider needs.
Identity providers — optional. IDP_AZURE_CLIENT_ID / IDP_GOOGLE_CLIENT_ID / IDP_OKTA_CLIENT_ID and their matching secrets. Leave a provider’s pair blank to leave that sign-in option off.
Registry credential — not in this file, on purpose. QR_REGISTRY_USERNAME and QR_REGISTRY_PASSWORD aren’t .env lines at all — the username contains a $, which Docker Compose would try to interpolate and mangle. Supply them as environment variables right before you run install.sh, or let it prompt you for them with input hidden.
Verifying what you received
verify.sh is offline and needs only openssl and sha256sum — no registry, no network:
- Checks the signature over the manifest, and that the key fingerprint matches the one recorded inside the signed bytes.
- Re-hashes every file in
bundle/against the manifest. - Re-hashes the compose project and install scripts.
- Confirms every image in
compose.yamlis digest-pinned and matches the manifest, in both directions. - Runs
cosign verifyon the images if cosign is present.
It exits 0 for trusted, 1 for not trusted, 2 for cannot verify.
Verification is also enforced at boot, not just at install: the auth service re-checks the signature and the file hashes before it serves policy, and refuses to start from a bundle it does not trust.
An export produced without a signing key configured is unsigned. It says so in its README and in the export warnings — treat an unsigned bundle as untrusted.
Connecting models
The appliance carries your LLM configuration — providers, routing by use case, the default config and your policies — but never your credentials. You supply those yourself, as QR_CFG_* entries in .env (see Filling in .env below for the naming pattern), one block per provider:
| Provider | What you supply |
|---|---|
| OpenAI, Anthropic, Cohere | API key |
| Azure OpenAI | API key, endpoint, API version |
| AWS Bedrock | IAM credentials in .env, or nothing at all if the host has an instance/task role |
| Google Vertex AI | Service account JSON in .env, or ambient credentials |
The appliance needs outbound access to whichever provider you configure. Bedrock via an ambient IAM role is the path that has been exercised end to end, and it ships no stored credentials at all — nothing to put in .env for it.
Knowledge-base search uses its own provider. If your project uses a knowledge base for retrieval-augmented answers, the embedding model that powers search is often a different provider than the one powering agent conversation — for example, Bedrock for chat and Vertex AI for embeddings. Supply credentials for both; the appliance warns at export time if a provider used by an included knowledge base has no matching credential.
Knowledge bases and schedules
Include a project’s knowledge bases (RAG stores) when you start the export, and their documents, chunks and embeddings travel inside the bundle — search works against them immediately on install, with no re-crawl and no connection back to qRaptor. Authoring a knowledge base (crawling new sources) still happens in the Studio; the appliance serves what was included at export time.
Any schedules on the agents you export travel too, and the appliance runs them itself from the moment it starts — there’s no separate step to turn them on. A schedule that was due while the appliance was down fires once to catch up and is skipped if it’s more than five minutes overdue, rather than queuing every missed run. The generated README.md lists every schedule that will start running, with its cron expression and timezone, so you know before you install that the box will begin doing work on its own.
Starting an agent from a row change, in an appliance
A database trigger is a managed-cloud feature — it doesn’t travel into an appliance export today, and an export doesn’t warn you when a project has one. If a project relies on a trigger to start an agent whenever a row changes, give it a Backend Call instead, before you export: it’s a server-side call your generated app’s own backend makes, and it travels with the app image because it’s part of your app’s code, not a separate rule the export has to carry.
Open the integration dialog on the relevant action
In your app’s chat panel, open the integration dialog from the action that currently writes the row — for example, the order-creation flow that inserts a new Order. See Open the integration dialog.
Choose Backend Call and your agent
Under Integration Type, click Backend Call, then pick the agent the trigger used to run.
Describe the call to match your trigger’s condition
Fill in Purpose and When so they describe exactly what the trigger watched for — for example, a trigger that fired on status CHANGED to shipped becomes a When of after an order's status is updated to shipped, run the agent with that order's details.
Add the integration and rebuild
Click Add Integration. qRaptor adds the call to your app’s server code, in the same place the row is written — so it runs identically whether the app is on the managed cloud or installed as an appliance.
Remove the original trigger, then export
Once the Backend Call is confirmed working in preview, delete the database trigger so the same event doesn’t fire the agent twice on the managed cloud. Export (or re-export) the project — the automation now travels with the app itself.
This also removes the one thing a trigger could do that a Backend Call can’t replace by itself: firing from a row change made outside your app, such as a direct SQL update or another service writing to the same table. A Backend Call only runs when your app’s own code path executes. If something outside your app needs to start the agent too, that write has to go through your app’s API instead of directly against the table.
Files and documents
Document upload and download in the generated app work inside the appliance, backed by a local folder on the install host rather than qRaptor’s document service — each file is written with a metadata sidecar, and a size ceiling you set in .env guards the host’s disk. There’s no external storage and no credentials to configure.
Documents already sitting in your project’s library at export time are not carried into the archive yet — only files uploaded through the running appliance afterward are stored.
Data and telemetry
Exports are schema only by default — you get the structure and an empty database. Including data is an explicit opt-in when you start the export.
Telemetry is off by default. You choose at export time:
- Disabled — no exporter is constructed.
- Your collector — spans go to an OTLP endpoint you name. Nothing is sent to qRaptor.
- Bundled — a collector runs inside the appliance and writes to its own disk. It publishes no host port.
LLM usage is metered into the appliance’s own database either way, not reported anywhere.
Upgrading an installed appliance
Download a newer archive for the same project and run install.sh again, over the same installation — there’s no separate upgrade tool.
install.shtakes a full database backup before touching anything, and stops if the backup fails rather than risk an upgrade with no way back.- Changed schema files are re-applied — new columns arrive with safe defaults. Data files are never replayed over an existing database, so your rows are left alone.
- The install records which version is running, so re-running the same archive a second time is a no-op for anything already applied.
To roll back, restore the backup install.sh took and re-run the previous archive. There’s no reverse migration — rollback is restore-and-reinstall, not an undo.
What is not supported yet
Appliance v1 is deliberately narrow. These are not available today:
- Air-gapped install. The install pulls from a registry; images are not shipped inside the archive.
- Kubernetes. Docker Compose only — no Helm chart or manifests.
- arm64 hosts. Images are
linux/amd64only. An arm64 host pulls successfully and then fails to execute. - Carrying a project’s existing documents. Document upload/download works once installed (see Files and documents above), but files already in your project’s library at export time aren’t included yet.
- Database triggers. See Starting an agent from a row change, in an appliance above for the Backend Call replacement.
- MCP tool catalogs and marketplace tools. These fail fast with a clear error rather than hanging. Built-in tools work, and MCP credentials from your configurations resolve.
- Voice.
- Federated / remote executors. The appliance runs work locally.
- External identity providers (SSO). Password and email login work out of the box; an exported app starts with phone login, OTP and self-registration off regardless of how your organization configured them.
- Studio. Nothing in the appliance authors agents or configurations; agent, system and schedule versions are pinned at export.
An export with no application tier (agents only) starts its runtime services but serves nothing at / — the export warns you when this is the case.
Common issues
- The export fails the coherence check. Run a dry run and read the specific error — it names the missing agent, empty table, or credential-less LLM route causing it. Fix the project and re-run the dry run before spending time on a real export.
- Login works, then times out or loops back to the sign-in page. This is almost always the hostname callout under Installing it: the appliance’s containers can’t call back out to their own public address. Add the
extra_hostsentry described there. install.shorverify.shrefuses to continue. A clean archive exits0. A refusal names what didn’t match — a tampered file, a key that doesn’t match the signature, or (in a dev export) no signature at all. Don’t override a refusal without knowing which of these it is.- A schedule or knowledge base you expected isn’t there. It has to be selected in the export wizard’s Contents step — nothing is included automatically. Re-export with it checked.
- An automation that used to run on a row change stopped working after install. That’s the trigger gap above, not a bug — see Starting an agent from a row change, in an appliance.
Related
- Remote executors — keep the platform managed, but run individual steps inside your own network.
- Deployments overview — the managed-cloud path.
- Data governance — the row, column and masking rules that travel inside the bundle.