Skip to Content

Installing an appliance

By the end of this page you will have a downloaded archive running on your own infrastructure, signing users in at the address you chose, with its models connected.

Prerequisites

  • A linux/amd64 host with Docker and Docker Compose v2.
  • The archive, from the export’s page in Studio. See Exporting a project.
  • A decision on how users will reach it — see the next section.

Choose how users will reach it

Decide this before you fill in .env. Sign-in only works if the appliance knows its own public address exactly, including whether it’s http or https.

How users reach itQR_PUBLIC_URLQR_GATEWAY_LOOPBACK_HOSTQR_COOKIE_SECURE
A hostname, plain HTTPhttp://apps.example.com:443apps.example.comfalse
A hostname with TLS (your own proxy or load balancer in front)https://apps.example.comSee belowtrue
A bare IP, plain HTTP (quick tests only)http://203.0.113.10:443blankfalse

Leave QR_COOKIE_SAMESITE=Lax in every case.

The appliance publishes one port, 443, and answers there in plain HTTP. For plain HTTP, users open that port directly, which is why the address includes :443.

Using a domain with TLS. The appliance doesn’t manage certificates. If you put your own TLS proxy or load balancer in front of it, send its traffic to the host’s port 443 as plain HTTP, then set:

  • QR_PUBLIC_URL to the https:// address users open,
  • QR_COOKIE_SECURE=true,
  • QR_GATEWAY_LOOPBACK_HOST to the hostname if the proxy runs on this host, or leave it blank if it runs on another machine. See QR_GATEWAY_LOOPBACK_HOST.

The certificate must come from a public certificate authority, such as Let’s Encrypt. A self-signed certificate makes sign-in fail after the password step, because the appliance itself calls its public address during sign-in and only trusts public certificates.

⚠️

A bare IP only works if the host can reach its own public IP. Most cloud VMs can’t — sign-in then times out after the password step, and QR_GATEWAY_LOOPBACK_HOST can’t help, because it only applies to hostnames. Use a hostname for anything beyond a quick test.

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, or from the public Signing keys reference — never from inside the archive itself, since a tampered archive could ship its own matching fake key.

The Appliance page's release key panel, showing the current Ed25519 public key, its fingerprint, and Copy and Download buttons

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.

A completed export's download panel, showing the archive's SHA-256, a Download button, and a button to mint a new pull credential

Unpack and fill in .env

tar -xzf appliance-<project>-<version>.tar.gz cd appliance-<project>-<version> cp .env.example .env

Fill in .env as described in Filling in .env. Start with the address values you chose above.

⚠️

Don’t reuse a .env from an older export as-is. A newer export can add settings — QR_GATEWAY_LOOPBACK_HOST is one. Start from the new .env.example and copy your values into it, or compare the two:

diff <(grep -o '^[A-Z_]*=' .env.example | sort) <(grep -o '^[A-Z_]*=' .env | sort)

Every line starting with < is a setting your .env is missing.

Verify, then install

./verify.sh /path/to/qraptor-release.pub QR_REGISTRY_USERNAME='robot$…' QR_REGISTRY_PASSWORD='…' ./install.sh /path/to/qraptor-release.pub

Both commands take the release public key from step 1 as their argument — not the pull credential. install.sh runs the same verification as verify.sh, logs in to the registry with your pull credential, pulls every image by digest, checks image signatures if you have cosign installed, and starts the appliance. If you leave out the two QR_REGISTRY_* variables, it prompts for them with input hidden.

💡

On macOS, the default /usr/bin/openssl is LibreSSL and can’t verify Ed25519 signatures. verify.sh detects this and tells you to install OpenSSL 3.x.

Check it works

Give the first boot a minute or two — it seeds the identity service and loads the database. Then:

docker compose ps

Every service should be running or healthy, apart from one-shot setup jobs (db-init, keycloak-seed, dms-import, agent-runtime-volume-init), which show exited (0). Open QR_PUBLIC_URL in a private browser window and sign in as admin with APP_ADMIN_INITIAL_PASSWORD. You’re asked to choose a new password on first sign-in.

See Signing keys for exactly what the release key, the pull credential and the cosign image signature each prove — mixing them up is the most common setup mistake.

To call the appliance’s agents from your own code, see Agent exposure. Nothing is exposed until you choose to.

Filling in .env

Work through .env 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 use. The identity service’s issuer, every sign-in redirect and the session cookies all derive from it, and a wrong value fails sign-in with no error in any log. Set it, QR_GATEWAY_LOOPBACK_HOST and the cookies from the table in Choose how users will reach it. QR_COOKIE_DOMAIN stays blank unless you serve several subdomains from one sign-in.

⚠️

QR_COOKIE_SAMESITE is case-sensitive — it must be Lax, Strict or None, capitalized exactly like that. A lowercase lax crashes the cookie writer on every sign-in attempt.

Database. QR_DB_NAME, QR_DB_USER and QR_DB_PASSWORD — generate a fresh password for this appliance, never reuse one from elsewhere.

Sign-in (Keycloak). KC_BOOTSTRAP_ADMIN / KC_BOOTSTRAP_ADMIN_PASSWORD create the one-time admin account used to set up sign-in 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; nobody types them in. APP_ADMIN_INITIAL_PASSWORD is the first sign-in password for the app’s admin user.

Signing. QR_RELEASE_PUBLIC_KEY holds qRaptor’s release public key, and QR_RELEASE_PUBLIC_KEY_FILE points at the same key as a file for verify.sh — see Signing keys. Leave them blank only for a throwaway test install; the appliance then boots with signature verification off, which it says loudly rather than hiding.

Service-to-service. DB_PROXY_HMAC_SECRET and QRUNX_JWT_SECRET are internal signing secrets. Generate a random value of 32 bytes or more for each, for example with openssl rand -hex 32.

File storage. QR_DMS_ENABLED turns document upload and download on or off. QR_DMS_MAX_UPLOAD_MB caps a single upload — it protects the host’s disk, it isn’t a plan limit.

Model and embedding credentials. One block per provider your project uses, named QR_CFG_<PROVIDER>_<CONFIG>_<FIELD> — for example an AWS Bedrock access key or a Google service-account JSON. The export only lists the ones your project’s configurations reference; see Connecting models.

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. The username contains a $, which Docker Compose would try to interpolate and mangle, and nothing in the running appliance needs it. Supply them as environment variables when you run install.sh, or let it prompt you.

QR_GATEWAY_LOOPBACK_HOST

What it fixes: sign-in failing with an error page right after the user enters their password.

When a user signs in, the browser goes to the identity service and comes back with a one-time code. The appliance’s gateway then exchanges that code for the user’s session by calling QR_PUBLIC_URL itself — from inside the host, not from the user’s browser. That call fails when:

  • the hostname only exists in your users’ hosts files or internal DNS, so the host can’t look it up, or
  • the host can’t connect to its own public address. Most cloud providers don’t route a machine’s traffic back to its own public IP.

Setting QR_GATEWAY_LOOPBACK_HOST to the hostname in QR_PUBLIC_URL tells the gateway to reach that name on this machine directly. Users’ browsers aren’t affected.

DoDon’t
Set it to the hostname only: apps.example.comInclude https://, a port or a path
Set it for plain HTTP on a hostname, or when your TLS proxy runs on this hostSet it when TLS ends on another machine — the sign-in call would hit the gateway’s own plain-HTTP port and fail
Recreate the gateway after changing it: docker compose up -dExpect it to help with a bare-IP address — it only applies to names

To check it’s in effect:

docker compose exec gateway cat /etc/hosts | grep apps.example.com

You should see the hostname mapped to a 172.x address. No output means the gateway hasn’t picked it up — check .env, then run docker compose up -d.

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 as QR_CFG_* entries in .env, one block per provider:

ProviderWhat you supply
OpenAI, Anthropic, CohereAPI key
Azure OpenAIAPI key, endpoint, API version
AWS BedrockIAM credentials in .env, or nothing at all if the host has an instance or task role
Google Vertex AIService account JSON in .env, or ambient credentials

The appliance needs outbound access to whichever provider you configure. Bedrock via an ambient IAM role ships no stored credentials at all — nothing to put in .env for it.

ℹ️

Knowledge-base search uses its own provider. The embedding model that powers knowledge-base 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 export warns if a provider used by an included knowledge base has no matching credential.

Files and documents

Document upload and download in the generated app work inside the appliance, backed by a Docker volume on the install host rather than qRaptor’s document service. There’s no external storage and no credentials to configure.

If the export included Documents and assets, the app’s existing files travel in the archive with their original IDs, and a one-time setup job (dms-import) copies them into place on first boot — so attachments already linked from your data, such as an order’s receipt, download as before. To confirm:

docker compose logs dms-import

It reports how many documents it imported. Files someone later deletes in the app stay deleted; the import only runs again when an upgrade brings a different set of documents. Only each document’s latest version travels — earlier versions don’t.

If the export left documents out, existing links to them answer “document not found”. Re-export with Documents and assets selected.

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.

  1. install.sh takes a full database backup before touching anything, and stops if the backup fails rather than risk an upgrade with no way back.
  2. 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.
  3. The install records which version is running, so re-running the same archive is a no-op for anything already applied.

Keep your own files when you upgrade: .env and config/api-keys.yaml. A new archive unpacked over them writes a fresh, all-off config/api-keys.yaml.

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.

Removing an appliance

From the archive folder:

docker compose down -v --remove-orphans

This deletes the appliance’s database, files and certificates along with its containers. Without -v, the data volumes stay — and a later install reuses them, including the old admin password.

Common issues

  • Sign-in fails with an error page right after the password step. The gateway can’t reach QR_PUBLIC_URL from inside the host. Set QR_GATEWAY_LOOPBACK_HOST, or for a bare IP, switch to a hostname.
  • The browser shows ERR_SSL_PROTOCOL_ERROR. You opened https://, but nothing in front of the appliance handles TLS, so the gateway answered in plain HTTP. Use the http://…:443 address, or put a TLS proxy in front as described in Choose how users will reach it.
  • After reinstalling, the old admin password still works and the new APP_ADMIN_INITIAL_PASSWORD doesn’t. The previous install’s data volumes survived. Run docker compose down -v before reinstalling, or keep using the old password.
  • Sign-in breaks after replacing .env. An older .env can be missing settings this export needs. Compare it with .env.example as shown in Unpack and fill in .env.
  • Agent exposure — calling the appliance’s agents from your own code.
  • Signing keys — what the release key, pull credential and cosign signature each prove.
  • Exporting a project — producing the archive you’re installing.