Skip to main content
Ragen runs entirely on your own infrastructure. This page walks you through the manual deployment path — what every service is for, the minimum environment you need to provide, and the settings that matter most before you go to production. If you want the fastest possible path to a running instance, start with create-ragen-app instead; come back here when you want to understand and control what it did.
To try Ragen on a managed platform first, Deploy on Railway deploys the same services from a template.

Prerequisites

Ensure the following are in place before you begin:
  • Docker and Docker Compose — all backing services run in containers.
  • Node.js ^24.15.0 || >=26.0.0 — required if you are running the applications outside of containers. This is a range, not a floor: 24.0–24.14 and the whole of 25 fail npm install on a transitive engine constraint.
  • 8 GB of RAM available to Docker to evaluate Ragen, 16 GB for a small production install. Most of that is headroom rather than steady-state: the containers a default docker compose up starts idle at about 900 MB together, and the rest covers document parsing under load and the applications themselves. What the stack actually costs breaks it down.
A GPU is only needed if you plan to serve language models on your own hardware. Everything else runs on CPU.

Manual deployment

1

Start the backing services

From the root of your cloned repository, bring up the full set of infrastructure services:
This starts Postgres, Qdrant, Redis, and Docling in Docker. The applications themselves run on the host with hot reload.
To run every application in containers as well, npm run ragen:up:everything builds and starts the web app, API, worker, and admin panel alongside all services.
npm run ragen:up:app is not a smaller way to run Ragen any more. It brings up Postgres and Qdrant without Redis, which was a working query-only stack while Temporal was the default runtime and Redis was merely a cache. Under bullmq it is not: apps/web and apps/api validate REDIS_URL at boot, whether or not you ever upload a document, because a producer that cannot reach the queue cannot enqueue at all. The API exits naming the variable and the web app serves its setup page.There is no supported configuration that drops Redis. Use ragen:up:full.
2

Install dependencies

Install Node.js dependencies across the entire monorepo:
3

Generate the Prisma client

The Prisma client is generated from the schema and is not committed to the repository. Nothing builds without it:
This step is not optional on a fresh checkout. Skipping it causes obscure build failures far removed from the actual cause.
4

Run database migrations

Apply the Prisma schema to your database:
This command reads DATABASE_URL directly from the environment — make sure that variable is set before you run it.
5

Seed the database

Populate the database with the default configuration that every new organization requires:
This step is not optional — without it, organization creation fails. It reads configuration from .env.local.
6

Start the applications

Start each application in a separate terminal. There is no single root dev command because these are separate processes with separate lifetimes:
apps/api is not optional. The web app delegates thread creation, the thread sidebar, and notifications to it. Running only the web app gives you a panel that loads and a chat that cannot open a thread.
apps/worker is what makes an uploaded document searchable. It is not in docker-compose.yml — it runs on the host and drains the job queues in Redis. Without it, uploads succeed and then sit unparsed indefinitely. The worker validates its own environment at boot and exits with the missing variable named rather than starting in a broken state.
apps/mcp is the fifth application, and it is optional: it exposes your assistants to MCP clients such as Claude Desktop and Cursor, and nothing in the web panel depends on it. It is a thin forwarder — no database, no Redis, no key of its own — but it does need apps/api reachable, because every tool call goes there with the caller’s own API key. Set RAGEN_MCP_PORT rather than PORT: three services in this repository read a bare PORT, and the root .env.local is shared by all of them. See MCP Server.

Applications

Five applications ship in the repository. Three of them make a working install; the other two add surfaces you may not need.

Services

Every service has a specific role in the stack. Removing one affects only the capabilities it provides.
Redis is not optional on the default runtime, and it is the variable most often missed. WORKER_RUNTIME defaults to bullmq, which puts the job queues in Redis: the worker drains them, and apps/web and apps/api write to them. An application that cannot reach Redis cannot enqueue a job, so an upload fails outright rather than being processed later. The Redis it points at must run maxmemory-policy noeviction — an evicting instance drops queue keys and the jobs go with them, which the worker refuses at boot rather than discovering in production. See Background jobs.

What the stack actually costs

Measured on an idle stack, backing services only: Two figures rather than one, because most of the total is optional. A plain docker compose up starts neither Presidio container, so unless you asked the installer for PII masking the containers cost you about 900 MB, not 1.9 GB. Qdrant is the line that moves as you add documents — size that one against your own corpus rather than against this table. The Node applications run on top of this and are not in the table. In development they are the larger half — each Next.js dev server is comfortably larger than Postgres — which is why the machine recommendation above sits well above the container total.

Trimming it further

With Docling and Presidio both out, the stack is Postgres, Redis and Qdrant — under 150 MB idle.
DOCUMENT_PARSER=legacy sends PDFs to an external model, and DOCLING_STRICT does not stop it. Strict mode governs the fallback from a failed Docling parse; with the legacy parser selected outright, Docling is never called and the strict check is never reached. If documents must not leave your network, Docling is not the component to trim — keep it, and set DOCLING_STRICT=1.

Published container images

Building five images on every install is work nobody needs to repeat. Each release publishes them to GitHub Container Registry, amd64:
Every image carries four tags: the full version (v1.4.2), the minor series (1.4), the exact commit (sha-…) and latest. Pin production to the sha- tag — latest moves, and so does the minor series. The images hold no per-install configuration. Everything they need comes from the environment at runtime, which is what makes one published image correct for every install; the configuration reference lists what to supply.
Three of these serve the default job runtime, and only that. ragen-web and ragen-api are BullMQ producers and ragen-worker is the consumer, which is what WORKER_RUNTIME defaults to. A deployment that sets WORKER_RUNTIME=temporal cannot use those three as published — the worker exits at boot and the two producers throw on the first upload. ragen-admin and ragen-mcp touch no queue and are unaffected. See Durable execution for what to run instead: the worker is an image swap, the producers are a rebuild.
npm run ragen:up:everything still builds from source, which is what you want while developing against a checkout. The published images are for deployments that are not tracking main.

Feature flags

Feature flags are environment variables that turn capabilities on or off. Set them in your .env.local file. The following flags are off by default — set them to 1 to enable: The following flag is on by default — set it to 0 to disable:

PII masking is configuration, not a flag

FEATURE_FLAG_PII_MASKING belongs in neither table above. Masking is on when both PRESIDIO_ANALYZER_URL and PRESIDIO_ANONYMIZER_URL are set, and the flag exists only to switch it off again:
Setting it to 1 enables nothing on its own. The two Presidio containers use roughly 1 GB of RAM together and sit behind --profile pii, which is why this is opt-in at all. See PII masking.
Multi-query expansion has no environment flag. It is a per-organization setting, defaulting to on, shown alongside document summaries and reranking under Organization → RAG settings. Those switches are read-only: a platform administrator sets them per organisation on the admin panel’s RAG Settings page.
Content moderation is not an environment variable, and the switch on that page is not what controls it. Moderation is the content-moderation guardrail rule: switch it on in the admin panel’s Guardrails page, and override it there for a single organisation if you want.MODERATION_ENABLED is read by nothing at runtime. If your installation still sets it, run npm run guardrails:preflight before deploying — it exits non-zero while your environment and the panel disagree about what is enforced — then remove the variable. OPENAI_MODERATION_KEY is still the credential the rule uses.Upgrading an on-premise installation carries the old per-organisation value over: where content moderation was off for an organisation, the migration wrote an override switching the rule off for it.

Minimum environment

The variables below are the minimum required for a working Ragen installation. Copy this block into your .env.local and replace each placeholder with a real value.
.env.local
The model lines above are a sketch, and the route table is the part they leave out. Ragen calls providers itself through a route table that maps each model id to an upstream, and the table shipped in the repository names Azure, Bedrock, Vertex and Scaleway — providers a new installation has no credentials for. A key alone therefore is not enough: the routes have to name the provider that key belongs to.The least error-prone way to get all of it consistent — key, model ids, embeddings model and its vector size, and a matching route table — is to scaffold the install with npx create-ragen-app, which asks which provider you have and writes the table for it. To wire it by hand instead, write your own file and point LLM_ROUTES_PATH at it.
Every <GENERATED_...> and <random-secret> placeholder must be replaced with its own unique, securely generated value. Do not share secrets between environments or use predictable values in production.
For production, add an ENCRYPTION_PROVIDER, a persistent storage path or S3 credentials, and DOCLING_STRICT=1 if documents must not leave your network. The full variable reference — including which provider makes which variable mandatory — is in the configuration reference.

Running without internet access

Ragen’s architecture is designed to support fully air-gapped deployments: the model layer is decoupled behind a route table or a proxy of your choosing, and document parsing is local by default. Two honest caveats apply before you commit to this path. It is deployment work, not a flag. Serving a capable model on your own hardware means provisioning GPU capacity. Locally-served open models generally perform less well than commercial ones on complex questions — how much less depends on your documents and your questions, so measure it on your own material first. Container images are pulled at install time. After that, outbound traffic can be cut entirely, with updates delivered as images to your internal registry.
When running without internet access, set all of the following:
  • DEFAULT_MODEL, EMBEDDINGS_MODEL, and any reranker configuration must point at your local endpoints
  • DOCLING_STRICT=1 — without this, a Docling failure silently sends documents to an external model
  • MAIL_PROVIDER=console — nothing is sent; an administrator creates each account directly and hands over the generated password out of band
Without DOCLING_STRICT=1, a single parsing failure is enough to send a document off your network.

Running in production

A local development setup is not suitable for production. Three areas require deliberate decisions before you expose Ragen to real users.

Encryption

In local development Ragen starts with no key provider and stores message content in plaintext. A deployed one does not. A process needs an encryption provider unless it is plainly not a deployment: NODE_ENV=development or test, or TARGET_ENV set to local, test, e2e or ci. Anywhere else — including a build that simply never set TARGET_ENV — a missing provider stops it: apps/api exits, apps/web serves a blocking page, and every content write throws. That is deliberate — the alternative silently writes readable messages until somebody thinks to check. Set ENCRYPTION_PROVIDER to one of the following values and supply the matching key material: Leaving ENCRYPTION_PROVIDER unset is not the same as having none: Ragen then auto-detects from whichever credentials are present, in the order Scaleway, KMS, local. Naming the provider is the readable choice. To run a deployed environment without encryption anyway — during a migration, say — set ALLOW_UNENCRYPTED=1. It is a conscious opt-out, logged as a critical security event on every start. It does not rescue a provider that is configured but broken; see Encryption. Verify encryption is active by creating a new thread after the change — if the API returns the thread without error and subsequent messages are readable, encryption is working correctly.

Storage

STORAGE_PROVIDER=local writes uploaded files to STORAGE_LOCAL_PATH (defaults to ./data/storage). Mount a persistent volume at that path, or a restart loses every uploaded document.
Do not use local storage in a multi-replica deployment. Replicas cannot read each other’s files. Ragen logs a warning at startup if you set TARGET_ENV=production or staging while using local storage.
For production and multi-replica setups, use STORAGE_PROVIDER=s3, which works with any S3-compatible object store:
.env.local
To run the object store yourself, docker compose --profile s3 up -d starts the bundled RustFS. The installer sets it up if you choose RustFS as the storage answer. See Storage.
Use S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY — not AWS_-prefixed variables. The AWS_ prefix is reserved for real AWS Bedrock and KMS configuration, which a deployment can use at the same time as non-AWS S3 storage.

Embedding model and vector dimensions

EMBEDDINGS_MODEL defaults to bge-multilingual-gemma2, which produces 3584-dimensional vectors. VECTOR_SIZE must match this value, or Qdrant rejects every upsert. If you switch to a different embedding model, update VECTOR_SIZE accordingly: VECTOR_SIZE has one default — 3584, the default model’s — and Ragen does not infer it from any other model name. Setting EMBEDDINGS_MODEL and leaving VECTOR_SIZE alone is therefore the mismatch above rather than a smaller configuration: change one and you must change the other. The minimum environment earlier on this page sets both together for that reason.
Changing the embedding model after documents are already indexed makes all existing vectors incompatible with the new model. You must re-index everything when you change EMBEDDINGS_MODEL.

Need help?

Web Amigos built Ragen and deploys it for companies across Europe. If you want a deployment set up, reviewed or maintained, talk to the team that built it.