Ragmux

Use cases

Air-gapped LLM deployment with Ragmux

Run an OpenAI-compatible gateway with retrieval fully inside a network with no outbound internet access, using local model servers and one self-contained PostgreSQL database.

Updated: Last reviewed: Verified against Ragmux 0.5.0

“Air-gapped” and “on-premise” usually mean the same underlying requirement stated two ways: nothing about a request, a document or an answer is allowed to leave a network boundary the operator controls, whether that boundary is a literal physical air gap or a private cloud VPC with no path to the public internet. Ragmux’s architecture — one binary, one database, and every provider connection pointed wherever the operator chooses — was never built assuming a cloud model provider is reachable, which makes it a workable fit for that requirement without any special “air-gapped mode” to turn on.

What actually has to be local

An AI gateway is only as air-gapped as the models behind it. Ragmux’s own translation layer works the same whether a connection’s provider_type is openai or ollama — the gateway doesn’t know or care whether a request is about to leave the network, only that it’s forwarding to whatever base_url the connection names. That means the deployment decision that actually determines whether this works isn’t about Ragmux at all: it’s whether a chat model and an embedding model are both running somewhere reachable inside the isolated network. ollama and custom_openai (for vLLM, LM Studio, or any other server speaking the OpenAI Chat Completions format) are the two provider types built for exactly that — everything downstream of the model connection, including retrieval, budgets and the audit log, works identically once that piece is in place.

The same local-only constraint applies to reranking, if a store uses it: cohere and voyage rerank backends call an external API and need the same outbound access a cloud model provider would, so they’re not available inside an isolated network. The llm backend — reranking with the project’s own chat model, which is already local in this deployment shape — has no such requirement, and is the one to reach for here.

The private-upstream guard exists for the opposite reason, and gets in the way here

Ragmux refuses, by default, to connect to a base_url that resolves to a loopback, link-local or private address — a deliberate SSRF guard against a dashboard editor pointing the gateway at an internal host it shouldn’t reach. In an air-gapped deployment, “internal host” is the whole point: the local Ollama or vLLM server is supposed to be a private address. PRIVATE_UPSTREAM_ALLOWLIST is what reconciles the two — naming the specific hostnames that may resolve privately (ollama, host.docker.internal, a Compose service name) without disabling the guard for every other connection the way ALLOW_PRIVATE_UPSTREAMS=true would. The right move for most air-gapped installs is the allowlist, not the blanket flag: it keeps the guard working for any connection an editor adds later that shouldn’t be allowed to reach an internal address.

What crosses the air gap once, and what never does

The one moment this deployment shape genuinely needs the outside world is before it becomes air-gapped at all: pulling the container image and verifying its signature. Images from v0.3.1 onward are signed with cosign in keyless mode, verified against Sigstore’s Fulcio service through the release workflow’s GitHub OIDC identity — which needs network access to check. The practical sequence is to do that verification on a connected host, then carry the verified image (ideally pinned to the digest cosign verify reports, so a later tag move can’t change what gets deployed) across the boundary with docker save and docker load or an equivalent internal registry mirror. Nothing about running the gateway afterward needs that connection again: SECRET_KEY encrypts credentials locally, the database is one self-contained PostgreSQL instance, and — per the exhaustive list in Configuration — there is no environment variable for a telemetry, license-check or update-check endpoint anywhere in Ragmux’s configuration surface. The dashboard itself is built to the same standard: it runs under a Content-Security-Policy strict enough, in the project’s own words, that it “makes no third-party requests” at all.

Data residency, and what it doesn’t mean

“On-premise AI gateway GDPR” is usually a data-residency question at heart: can an organization keep model traffic, and whatever those requests are grounded in, inside infrastructure it controls rather than sending it to a provider’s cloud. Running Ragmux air-gapped answers that directly — documents are stored as bytea in your own PostgreSQL, embeddings live in the same database, provider credentials are AES-256-GCM encrypted with a key you hold, and request logs never leave the instance unless an admin exports them. What it does not do is substitute for a compliance program: Ragmux carries no GDPR, SOC2 or HIPAA certification of its own, because it is AGPL-3.0 software published without a vendor behind it to go through that process — a fact worth being plain about rather than letting “self-hosted” imply “certified.” The audit log (every create, update, delete, login and key rotation, with actor, target and IP, exportable as NDJSON) is the kind of evidence a compliance review would want to see; it is not itself proof of compliance with anything.

What this deployment shape gives up

A single Ragmux instance on an isolated network gains nothing from the parts of the architecture built around cloud reachability. cosign verify needs to happen before the air gap, not continuously against it. A route can hand a failed request to its next target, but that only helps when a second target is reachable — a local model server going down is not covered by switching to a hosted one, because there usually isn’t a hosted one reachable to switch to. And an organization relying on this shape for a genuinely isolated network should plan its own process for getting security patches in: the gateway has no update-check mechanism to tell an operator a new release exists, by design, since that would itself be an outbound call the environment doesn’t allow.

A worked example: a hospital network’s internal assistant

Take a concrete shape of this: a hospital network building an assistant over its own clinical policy documents, on a network segment with no route to the public internet by policy, not just by convenience. The chat and embedding models both run locally — an ollama connection serving a model already approved for that environment, listed in PRIVATE_UPSTREAM_ALLOWLIST by its Compose service name so the outbound guard accepts it. Documents are uploaded once, ingested entirely inside the network (parsing, chunking and embedding are local computation against the local model, with nothing to reach outside for), and every subsequent question is answered from that store with no request ever leaving the segment. The one step that touched the outside world was verifying the container image’s signature before it was ever copied onto that segment in the first place — a deliberate, one-time crossing rather than an ongoing dependency.

Running more than one replica behind the air gap

Nothing about scaling out changes once a deployment is isolated. Several Ragmux replicas can sit behind a plain load balancer inside the same segment, sharing rate limits, budgets, sessions and the document-ingestion queue through one PostgreSQL database — the same story documented for any multi-replica Ragmux deployment, with no Redis or other shared cache tier to add. The one setting that matters more here than elsewhere is SECRET_KEY: every replica has to start with the exact same value, since a mismatch is a boot failure by design (the gateway seals a canary value into the database on first start and every replica after that verifies against it), and there is no external secrets service to fetch it from automatically in a network with no path to one — it has to be provisioned to every replica through whatever internal mechanism the environment already uses for secrets.

Where this doesn’t fit

  • A network that’s air-gapped except for the LLM calls themselves. If cloud model providers are still reachable and only the gateway’s own control plane needs to stay internal, that’s a much smaller constraint than this page assumes, and a hosted or cloud-adjacent connection type works fine alongside a self-hosted gateway.
  • A compliance requirement for a certification Ragmux doesn’t hold. Self-hosting satisfies data residency; it does not produce a SOC2 report or a GDPR attestation on its own, and a program that specifically needs one should evaluate what building that attestation around a self-hosted, uncertified project actually takes.
  • Automatic patching. An air-gapped install has no update-check mechanism to lean on; keeping current means an operator deliberately re-running the verify-and-transfer steps above on a schedule they own.

Deploy Ragmux in an air-gapped network

  1. Pull and verify the image on a network-connected host, then transfer it

    Pull ragmux/ragmux:<version> from Docker Hub (or the ghcr.io mirror) where internet access exists, verify it with cosign verify against the release workflow's identity, then move the verified image — or better, the manifest digest cosign verify reports — across the air gap with docker save / docker load. Verification itself needs the network; do it before the transfer, not after.

  2. Point every model and embedding connection at a local server

    Add connections with provider_type ollama, or custom_openai for vLLM, LM Studio or another OpenAI-compatible server running inside the same isolated network. No cloud provider connection will resolve without outbound access, so every chat and embedding model a project uses has to be reachable from inside the air gap.

  3. Allow those local hosts through the private-upstream guard

    A base_url that resolves to a private or loopback address is refused by default. Add the local server's hostname to PRIVATE_UPSTREAM_ALLOWLIST (for example ollama or a Compose service name) before creating the connection, rather than disabling the guard instance-wide with ALLOW_PRIVATE_UPSTREAMS. The allowlist matches hostnames only — an IP literal such as 192.168.1.50 is never matched, so a local server has to be reachable by a name, not just an address, for the allowlist to work.

  4. Set SECRET_KEY explicitly and keep it with your offline backups

    Generate it with openssl rand -hex 32 and pass it as an environment variable rather than relying on the generated secret.key fallback, which is documented for local development only. In a network with no path back to a key-management service, SECRET_KEY and your database backup together are the entire recovery story.

  5. Create the first administrator without the setup form

    Set ADMIN_USER and ADMIN_PASSWORD before first start so the account exists on boot; the interactive first-run setup form still works, but an unattended install behind an air gap has nobody to click through it.

  6. Put a reverse proxy in front and turn on the audit trail

    Terminate TLS at a reverse proxy inside the isolated network, set TRUST_PROXY_HEADERS and SECURE_COOKIES accordingly, and leave AUDIT_RETENTION_DAYS at its default (365 days) or raise it if a compliance review needs a longer window than that.

Frequently asked questions

Can Ragmux run with no outbound internet access at request time?
Yes — pair every model and embedding connection with a local server (Ollama, vLLM or another OpenAI-compatible endpoint) reachable inside the same network, and no request ever needs to leave it. The one exception is the initial cosign signature check, which needs network access and should happen before the image crosses the air gap, not after.
Does Ragmux call home, phone out, or send telemetry anywhere on its own?
Nothing in Ragmux's documented environment variables describes a telemetry, license-check or update-check call. The configuration reference lists every variable the gateway reads, and none names an analytics or phone-home endpoint — its only outbound calls are to the provider and rerank endpoints an admin explicitly configures.
How does an air-gapped Ragmux deployment help with GDPR data residency?
Running Ragmux inside infrastructure you physically control keeps documents, embeddings, request logs and provider credentials on that infrastructure rather than a vendor's. That is the data-residency question GDPR's cross-border transfer rules are actually about, though it is not the same as a GDPR compliance certification, which Ragmux does not carry.
How does a signed Ragmux image get onto a network with no outbound access at all?
It's verified with cosign on a connected host first, then carried across the boundary as a file. Pull the image where internet access exists, run cosign verify against the release workflow's identity, then move the verified image — ideally pinned to the digest cosign verify reports — with docker save and docker load or an internal registry mirror, since nothing about running the gateway afterward needs that connection again.
Do local model servers need special configuration to work behind Ragmux's outbound-connection guard?
Yes — a base_url that resolves to a private or loopback address is refused by default as an SSRF guard, with an explicit error naming the host and the two ways to allow it. A local Ollama or vLLM hostname has to be added to PRIVATE_UPSTREAM_ALLOWLIST before the connection is created, and the allowlist matches hostnames only — an IP literal like 192.168.1.50 is never matched, so a server reachable only by address still needs a resolvable hostname for this to work.
Does Ragmux's audit log support an internal compliance review?
It covers every create, update, delete, login, key rotation and password change with the actor, target and IP address recorded, exportable as NDJSON. It is a record of administrative actions, not a certification, and an auditor should treat it as exactly that.

Arrow keys to move, Enter to open.