π 100% local RAG system with one-command setup. Your data never leaves your server.
See the codeAsk questions about your own documents. Everything runs on your machine.
A self-hosted Retrieval-Augmented Generation system: upload your documents, ask questions in plain language, get answers grounded in what those documents actually say, with the sources cited. No data ever leaves your server β there is no telemetry, and nothing is sent to an external API.
This is a complete rewrite in Rust. The
1.xreleases were a Python stack: four containers under Docker Compose β backend, frontend, Qdrant, Ollama β two of them built on your machine, plus seven volumes to keep straight. This is one binary. It starts Qdrant and the inference engine itself; there is nothing to compose and nothing to build.The rewrite restarts the numbering at
0.1, so it is lower-numbered than the1.xline it replaces: it is a new codebase whose storage layout and HTTP surface are not settled yet. The Python version is preserved on thepython-legacybranch and in the1.xtags β see Upgrading from 1.x.
Most RAG systems either send your documents to somebody else's API, or ask you to orchestrate five containers before you can ask a single question. This does neither.
Built for people who cannot use cloud RAG for regulatory, privacy or data-sovereignty reasons: law firms, healthcare, finance, public administration.
Download the tarball for your platform from the releases page β CHANGELOG.md records what changed in each one:
| File | For |
|---|---|
linux-x86_64 | Linux PC or server (x86_64) |
linux-arm64 | ARM64 board |
windows-x86_64 | Windows PC |
Every tarball runs the embedding step (bge-m3, via Candle, inside this binary) on CPU. That does not mean no GPU is used: eullm β a separate process, started and kept up to date automatically β detects an NVIDIA GPU at startup and uses it, on every platform, for the chat model and, since eullm 0.6.82, for embedding too when asked (see Architecture). No separate GPU build or download is needed on any platform.
mkdir i3k-rag-engine-v0.1.44-linux-x86_64
tar -xzf i3k-rag-engine-v0.1.44-linux-x86_64.tar.gz -C i3k-rag-engine-v0.1.44-linux-x86_64
cd i3k-rag-engine-v0.1.44-linux-x86_64
printf 'AUTH__JWT_SECRET=%s\n' "$(openssl rand -hex 32)" > .env
./i3k-rag-engine
(the tarball's own contents are flat β binary, .env.example, frontend/dist/ β with
no wrapping folder inside it, so mkdir first, or extract straight into wherever
you want the install to live.)
On first run it downloads roughly 12 GB of components and verifies each one against a pinned sha256 β mostly the language and embedding models. Allow time for it. Every later start is immediate.
When it is ready your browser opens at http://localhost:8000. The initial admin password is printed in the log:
SAVE THIS PASSWORD β it will not be shown again!
Set AUTH__ADMIN_DEFAULT_PASSWORD in .env beforehand if you would rather
choose it yourself.
DATA__DIR defaults to the directory holding the executable (see
Configuration below), and every component β qdrant, eullm, the
chat and embedding models β is fetched into it only if missing or failing its
pinned sha256 (bootstrap::ensure_component). So extracting a new release's
tarball over an existing install (same directory, overwriting the binary,
.env.example, frontend/dist/) reuses everything already downloaded there β
nothing is re-fetched unless that release actually pins a different version of
it. Your own .env is never touched (the tarball only ever ships
.env.example). Extracting into a fresh, empty directory instead β the Quick
start default β starts every component's download over from scratch, since
nothing at that new path exists yet.
Documents. PDF (including scanned pages, via OCR), DOCX, XLSX, HTML, TXT, Markdown and CSV. Scanned PDFs are detected automatically β when a page yields too little text it is rasterised and passed through Tesseract in Italian and English.
Retrieval. Documents are split into overlapping chunks, embedded with BAAI/bge-m3 (1024 dimensions, multilingual across 100+ languages) and stored in Qdrant. A question is embedded the same way and answered from the closest passages, with each source shown.
Chat. Conversations are kept, answers stream token by token, and history is carried into follow-up questions.
Users. JWT authentication with three roles β user, super user, admin β governing who may upload and delete.
Backups. A scheduled daily archive holding the SQLite database and a Qdrant
snapshot taken together, so the pair is consistent, and an admin endpoint to
restore one over the running installation. Backups are local files under
BACKUP__DIR; nothing is uploaded anywhere.
| API and server | Rust, axum |
| Frontend | React + Vite, compiled into the binary's directory |
| Embeddings | BAAI/bge-m3 through Candle, in-process, GPU or CPU |
| Vector database | Qdrant, 1024-dim, cosine distance |
| Language model | eullm as a separate process |
| Application database | SQLite through sqlx β users, documents, conversations |
| OCR | pdfium for rasterising, Tesseract β both loaded at runtime from a bundled path, not linked at compile time |
The embedding model is loaded before the language model on purpose: eullm sizes its own GPU offload from the free VRAM it observes at startup, so it has to see the memory the embedding model has already taken.
Settings come from the environment or a .env file, using __ to separate
levels. Exactly one has no default:
AUTH__JWT_SECRET=β¦ # required β signs the session tokens
Everything else is optional:
SERVER__HOST=127.0.0.1 # loopback by default β see "Reaching it from
# another machine" below before changing it
SERVER__PORT=8000
SERVER__CORS_ORIGINS=β¦ # empty by default, and empty is right in
# production; set it only for a dev server
# on another port
AUTH__ADMIN_DEFAULT_PASSWORD=β¦ # seeds a NEW install; ignored once the admin
# exists. Otherwise a random one is logged
AUTH__ADMIN_RESET_PASSWORD=β¦ # deliberately overwrites it β for being
# locked out. Unset it once used
EULLM__MODEL=qwen3-14b # only read when you run eullm yourself; when
# the engine starts it, the GGUF path from
# manifest.toml is used instead
QDRANT__COLLECTION=rag_documents
EMBEDDINGS__REQUIRE_GPU=false # true = refuse to start without CUDA rather
# than silently falling back to a much slower CPU
DATA__DIR=/path/to/data # defaults to the binary's own directory
RUST_LOG=info
EMBEDDINGS__REQUIRE_GPU is worth knowing about: without it, a broken CUDA
setup degrades to CPU and ingestion goes from seconds to minutes, which is easy
not to notice. The fallback is always logged at error level and exposed on
GET /api/info.
SERVER__HOST defaults to 127.0.0.1, so out of the box the engine answers
only on the machine it runs on β which is what the quick start above describes.
Setting it to 0.0.0.0 makes it reachable from the network, and there is one
thing to understand before doing that: this binary serves plain HTTP and
terminates no TLS. On a routable address the login password and every
session token that follows cross the network in clear text, readable by anyone
who can see the traffic. The server logs a warning at startup whenever
SERVER__HOST is not a loopback address, for exactly this reason.
So expose it behind something that terminates TLS. With Caddy that is a whole configuration file:
rag.example.com {
reverse_proxy 127.0.0.1:8000
}
Leave SERVER__HOST at 127.0.0.1 in that setup: the proxy reaches the engine
over loopback, and nothing but the proxy can reach it directly.
The binary can benchmark itself on your own hardware and document:
./i3k-rag-engine --bench /path/to/document.pdf
It writes a Markdown report timing each stage of ingestion and inference β
extraction, chunking, embedding, upsert, prefill, decode β with charts showing
where the time goes. --bench-live instead records every real ingestion and
query of a session and reports on shutdown.
There is no automatic migration path, because the rewrite changes both the
storage layout and the runtime model: 1.x kept its state in Docker volumes
shared between containers, 0.1.x keeps it in one data directory next to the
binary.
The two cannot run at the same time on one machine: they both want ports 8000,
6333 and 11434. Stop the Compose stack first β you are re-uploading anyway, so
there is no moment where you need both. Your 1.x data stays in its Docker
volumes until you remove them, so you can go back by bringing Compose up again.
Once you are satisfied, docker compose down -v retires it for good.
The 1.x Python version remains available on the
python-legacy branch and in the 1.x tags. It is
no longer developed, but nothing has been deleted.
Most people do not need to: the release tarballs are self-contained. If you want to modify the engine or target a platform we do not publish, see BUILD.md for the native dependencies and the feature flags.
cargo build --release --features cuda
manifest.toml; a mismatch aborts the run.429 without costing a hash. Scripted clients should expect both:
a login can take a few seconds, or come back 429 and be worth retrying.Please report vulnerabilities privately rather than through a public issue.
@software{i3k_rag_engine,
author = {Marchetti, Francesco},
title = {i3k RAG Engine: self-hosted document intelligence},
publisher = {I3K Technologies Srl},
url = {https://github.com/I3K-IT/RAG-Enterprise}
}
The archived 1.x Python release has its own DOI: 10.5281/zenodo.20413005.
AGPL-3.0-only β see LICENSE.
You can run it, read it, change it and share it. The one obligation worth knowing before you build on it is AGPL section 13: if you offer a modified version to users over a network, you have to offer those users the corresponding source too. Running it privately β inside your own company, on your own documents β carries no such obligation, which is the normal case for a self-hosted system like this one.
I3K Technologies Srl holds the copyright and also offers this software under a separate commercial licence, for organisations that cannot accept the AGPL's terms. That same licence is what lets its own proprietary product, i3k RAG Pro, build on this code. Enquiries: info@i3k.eu
Up to and including v0.1.39 this project was published under the Apache License 2.0, and those releases remain available under those terms. The change applies from the following release onward β it does not withdraw anything already released.
Because the project is licensed both ways, contributions need a Contributor Licence Agreement β a one-line statement in your pull request. You keep the copyright in your own work; the CLA explains exactly what it grants and why it is necessary.
Third-party components, including the models downloaded at runtime, keep their own licences and are not covered by the AGPL: see THIRD_PARTY_LICENSES.md.
Built on Candle, Qdrant, axum, eullm, BAAI/bge-m3, Qwen, Tesseract and PDFium.
Made in the EU by i3k.
314 commits
14 commits
Rust
89.2%
JavaScript
10.1%
π 100% local RAG system with one-command setup. Your data never leaves your server.
See the codeAsk questions about your own documents. Everything runs on your machine.
A self-hosted Retrieval-Augmented Generation system: upload your documents, ask questions in plain language, get answers grounded in what those documents actually say, with the sources cited. No data ever leaves your server β there is no telemetry, and nothing is sent to an external API.
This is a complete rewrite in Rust. The
1.xreleases were a Python stack: four containers under Docker Compose β backend, frontend, Qdrant, Ollama β two of them built on your machine, plus seven volumes to keep straight. This is one binary. It starts Qdrant and the inference engine itself; there is nothing to compose and nothing to build.The rewrite restarts the numbering at
0.1, so it is lower-numbered than the1.xline it replaces: it is a new codebase whose storage layout and HTTP surface are not settled yet. The Python version is preserved on thepython-legacybranch and in the1.xtags β see Upgrading from 1.x.
Most RAG systems either send your documents to somebody else's API, or ask you to orchestrate five containers before you can ask a single question. This does neither.
Built for people who cannot use cloud RAG for regulatory, privacy or data-sovereignty reasons: law firms, healthcare, finance, public administration.
Download the tarball for your platform from the releases page β CHANGELOG.md records what changed in each one:
| File | For |
|---|---|
linux-x86_64 | Linux PC or server (x86_64) |
linux-arm64 | ARM64 board |
windows-x86_64 | Windows PC |
Every tarball runs the embedding step (bge-m3, via Candle, inside this binary) on CPU. That does not mean no GPU is used: eullm β a separate process, started and kept up to date automatically β detects an NVIDIA GPU at startup and uses it, on every platform, for the chat model and, since eullm 0.6.82, for embedding too when asked (see Architecture). No separate GPU build or download is needed on any platform.
mkdir i3k-rag-engine-v0.1.44-linux-x86_64
tar -xzf i3k-rag-engine-v0.1.44-linux-x86_64.tar.gz -C i3k-rag-engine-v0.1.44-linux-x86_64
cd i3k-rag-engine-v0.1.44-linux-x86_64
printf 'AUTH__JWT_SECRET=%s\n' "$(openssl rand -hex 32)" > .env
./i3k-rag-engine
(the tarball's own contents are flat β binary, .env.example, frontend/dist/ β with
no wrapping folder inside it, so mkdir first, or extract straight into wherever
you want the install to live.)
On first run it downloads roughly 12 GB of components and verifies each one against a pinned sha256 β mostly the language and embedding models. Allow time for it. Every later start is immediate.
When it is ready your browser opens at http://localhost:8000. The initial admin password is printed in the log:
SAVE THIS PASSWORD β it will not be shown again!
Set AUTH__ADMIN_DEFAULT_PASSWORD in .env beforehand if you would rather
choose it yourself.
DATA__DIR defaults to the directory holding the executable (see
Configuration below), and every component β qdrant, eullm, the
chat and embedding models β is fetched into it only if missing or failing its
pinned sha256 (bootstrap::ensure_component). So extracting a new release's
tarball over an existing install (same directory, overwriting the binary,
.env.example, frontend/dist/) reuses everything already downloaded there β
nothing is re-fetched unless that release actually pins a different version of
it. Your own .env is never touched (the tarball only ever ships
.env.example). Extracting into a fresh, empty directory instead β the Quick
start default β starts every component's download over from scratch, since
nothing at that new path exists yet.
Documents. PDF (including scanned pages, via OCR), DOCX, XLSX, HTML, TXT, Markdown and CSV. Scanned PDFs are detected automatically β when a page yields too little text it is rasterised and passed through Tesseract in Italian and English.
Retrieval. Documents are split into overlapping chunks, embedded with BAAI/bge-m3 (1024 dimensions, multilingual across 100+ languages) and stored in Qdrant. A question is embedded the same way and answered from the closest passages, with each source shown.
Chat. Conversations are kept, answers stream token by token, and history is carried into follow-up questions.
Users. JWT authentication with three roles β user, super user, admin β governing who may upload and delete.
Backups. A scheduled daily archive holding the SQLite database and a Qdrant
snapshot taken together, so the pair is consistent, and an admin endpoint to
restore one over the running installation. Backups are local files under
BACKUP__DIR; nothing is uploaded anywhere.
| API and server | Rust, axum |
| Frontend | React + Vite, compiled into the binary's directory |
| Embeddings | BAAI/bge-m3 through Candle, in-process, GPU or CPU |
| Vector database | Qdrant, 1024-dim, cosine distance |
| Language model | eullm as a separate process |
| Application database | SQLite through sqlx β users, documents, conversations |
| OCR | pdfium for rasterising, Tesseract β both loaded at runtime from a bundled path, not linked at compile time |
The embedding model is loaded before the language model on purpose: eullm sizes its own GPU offload from the free VRAM it observes at startup, so it has to see the memory the embedding model has already taken.
Settings come from the environment or a .env file, using __ to separate
levels. Exactly one has no default:
AUTH__JWT_SECRET=β¦ # required β signs the session tokens
Everything else is optional:
SERVER__HOST=127.0.0.1 # loopback by default β see "Reaching it from
# another machine" below before changing it
SERVER__PORT=8000
SERVER__CORS_ORIGINS=β¦ # empty by default, and empty is right in
# production; set it only for a dev server
# on another port
AUTH__ADMIN_DEFAULT_PASSWORD=β¦ # seeds a NEW install; ignored once the admin
# exists. Otherwise a random one is logged
AUTH__ADMIN_RESET_PASSWORD=β¦ # deliberately overwrites it β for being
# locked out. Unset it once used
EULLM__MODEL=qwen3-14b # only read when you run eullm yourself; when
# the engine starts it, the GGUF path from
# manifest.toml is used instead
QDRANT__COLLECTION=rag_documents
EMBEDDINGS__REQUIRE_GPU=false # true = refuse to start without CUDA rather
# than silently falling back to a much slower CPU
DATA__DIR=/path/to/data # defaults to the binary's own directory
RUST_LOG=info
EMBEDDINGS__REQUIRE_GPU is worth knowing about: without it, a broken CUDA
setup degrades to CPU and ingestion goes from seconds to minutes, which is easy
not to notice. The fallback is always logged at error level and exposed on
GET /api/info.
SERVER__HOST defaults to 127.0.0.1, so out of the box the engine answers
only on the machine it runs on β which is what the quick start above describes.
Setting it to 0.0.0.0 makes it reachable from the network, and there is one
thing to understand before doing that: this binary serves plain HTTP and
terminates no TLS. On a routable address the login password and every
session token that follows cross the network in clear text, readable by anyone
who can see the traffic. The server logs a warning at startup whenever
SERVER__HOST is not a loopback address, for exactly this reason.
So expose it behind something that terminates TLS. With Caddy that is a whole configuration file:
rag.example.com {
reverse_proxy 127.0.0.1:8000
}
Leave SERVER__HOST at 127.0.0.1 in that setup: the proxy reaches the engine
over loopback, and nothing but the proxy can reach it directly.
The binary can benchmark itself on your own hardware and document:
./i3k-rag-engine --bench /path/to/document.pdf
It writes a Markdown report timing each stage of ingestion and inference β
extraction, chunking, embedding, upsert, prefill, decode β with charts showing
where the time goes. --bench-live instead records every real ingestion and
query of a session and reports on shutdown.
There is no automatic migration path, because the rewrite changes both the
storage layout and the runtime model: 1.x kept its state in Docker volumes
shared between containers, 0.1.x keeps it in one data directory next to the
binary.
The two cannot run at the same time on one machine: they both want ports 8000,
6333 and 11434. Stop the Compose stack first β you are re-uploading anyway, so
there is no moment where you need both. Your 1.x data stays in its Docker
volumes until you remove them, so you can go back by bringing Compose up again.
Once you are satisfied, docker compose down -v retires it for good.
The 1.x Python version remains available on the
python-legacy branch and in the 1.x tags. It is
no longer developed, but nothing has been deleted.
Most people do not need to: the release tarballs are self-contained. If you want to modify the engine or target a platform we do not publish, see BUILD.md for the native dependencies and the feature flags.
cargo build --release --features cuda
manifest.toml; a mismatch aborts the run.429 without costing a hash. Scripted clients should expect both:
a login can take a few seconds, or come back 429 and be worth retrying.Please report vulnerabilities privately rather than through a public issue.
@software{i3k_rag_engine,
author = {Marchetti, Francesco},
title = {i3k RAG Engine: self-hosted document intelligence},
publisher = {I3K Technologies Srl},
url = {https://github.com/I3K-IT/RAG-Enterprise}
}
The archived 1.x Python release has its own DOI: 10.5281/zenodo.20413005.
AGPL-3.0-only β see LICENSE.
You can run it, read it, change it and share it. The one obligation worth knowing before you build on it is AGPL section 13: if you offer a modified version to users over a network, you have to offer those users the corresponding source too. Running it privately β inside your own company, on your own documents β carries no such obligation, which is the normal case for a self-hosted system like this one.
I3K Technologies Srl holds the copyright and also offers this software under a separate commercial licence, for organisations that cannot accept the AGPL's terms. That same licence is what lets its own proprietary product, i3k RAG Pro, build on this code. Enquiries: info@i3k.eu
Up to and including v0.1.39 this project was published under the Apache License 2.0, and those releases remain available under those terms. The change applies from the following release onward β it does not withdraw anything already released.
Because the project is licensed both ways, contributions need a Contributor Licence Agreement β a one-line statement in your pull request. You keep the copyright in your own work; the CLA explains exactly what it grants and why it is necessary.
Third-party components, including the models downloaded at runtime, keep their own licences and are not covered by the AGPL: see THIRD_PARTY_LICENSES.md.
Built on Candle, Qdrant, axum, eullm, BAAI/bge-m3, Qwen, Tesseract and PDFium.
Made in the EU by i3k.
314 commits
14 commits
Rust
89.2%
JavaScript
10.1%