I built a private AI knowledge base (RAG) on my own server. Now what?

It works. I upload a file, my own server chops it up and turns it into numbers, and when I ask a question, an AI answers from my own notes. First proper test: about 6 seconds and 1,271 tokens.

I built it because every AI tool wants my documents. Upload your handbook, your price list, your notes, and a friendly chatbot will answer questions about them. Lovely. Also: my documents, on someone else's server.

So I did what any reasonable person with a Proxmox box under the desk would do. I built it myself, and broke it twelve different ways along the way.

Which leaves the awkward question in the title. I have a private AI knowledge base. Now what? Here is what I built, what broke, what the numbers look like, and what I think it is actually for. Including, if you run a business, whether you should have one too.

The stack at a glance

Four services on one virtual machine do everything except write the final answer. That job goes to Gemini.

Only the answer-writing leaves my server Only the answer-writing leaves my server My server Proxmox VM, Coolify, 32 GB RAM You n8n Upload form and chat Glues everything together Ollama nomic-embed-text, on CPU Text in, 768 numbers out MongoDB 8.2 + mongot Stores chunks and vectors Runs $vectorSearch Gemini API Paid tier Writes answers question + 4 chunks answer Also on the box: Open WebUI for poking at Ollama, PostgreSQL for n8n
The whole stack: 4 services on one VM, 1 external API.

The hardware is an old Intel Core i7-6700 running Proxmox, with a 32 GB virtual machine that runs Coolify. Coolify is a self-hosted Heroku: you paste a Docker Compose file, it runs it. Mostly.

The split matters. My documents and every vector stay at home. What leaves is your question plus the four chunks of text that match it. Gemini never sees the whole file, just the four bits that look relevant.

Why not run the answer model locally too? Because a 3-billion-parameter model on a 2015 CPU would answer at roughly the speed of a fax machine. Embedding is cheap enough for a CPU. Writing good answers is not.

Workflow 1: feeding it documents

One upload through a web form turns into 16 chunks in MongoDB, each with 768 numbers attached. That is the whole trick behind RAG: turn text into numbers once, so you can search by meaning later.

One upload becomes 16 searchable chunks One upload becomes 16 searchable chunks Upload form n8n Form Trigger .pdf, .md or .txt Load the file Default Data Loader Pulls the text out Split into chunks 1,000 characters each 200 characters overlap Embed each chunk Ollama, nomic-embed-text 768 numbers per chunk Store in MongoDB rag.notes, batches of 200 text + embedding First document 16 chunks stored Ready to be searched Still missing: each chunk should also carry the name of the file it came from
The ingest workflow in n8n: 6 steps, first run on 1 document.

The steps, in plain English:

  1. Upload. A simple n8n form with one file field. Use n8n's built-in Form trigger, not the community one with almost the same name. Ask me how I know.
  2. Load. The data loader reads the PDF, Markdown or text file and pulls out the words.
  3. Split. The text gets cut into 1,000-character pieces that overlap by 200 characters. More on why in the next section.
  4. Embed. Ollama runs nomic-embed-text and turns each piece into a list of 768 numbers. Pieces that mean similar things get similar numbers.
  5. Store. Each chunk lands in the rag.notes collection as two fields: text and embedding.

The missing step is labelling. Right now the chunks do not remember which file they came from, so answers end with "no source file name provided". That is the AI version of "I read it somewhere". Fixing it is one metadata field and a re-upload.

Chunks and overlap, explained with numbers

Each chunk is 1,000 characters and repeats the last 200 characters of the one before. That way a sentence that falls on a boundary still turns up whole in at least one chunk.

Why chunk at all? Because the embedding squashes a whole piece of text into one point of meaning. Embed an entire handbook as one vector and you have described a novel with one word. Go too small and each chunk is a lonely sentence with no context. Around 1,000 characters, a few paragraphs, is a sensible middle.

Each chunk starts 200 characters before the last one ended Each chunk starts 200 characters before the last one ended 0 500 1,000 1,500 2,000 2,500 Chunk 1 Chunk 1: characters 0 to 1,000 Chunk 2 Chunk 2: characters 800 to 1,800 Shared with chunk 1: 200 characters Chunk 3 Chunk 3: characters 1,600 to 2,600 Shared with chunk 2: 200 characters Text that appears in two chunks Position in the document, in characters
My splitter settings: the first 3 chunks of a document.

Overlap is insurance, and like all insurance it costs something. With 200 characters of overlap the splitter only moves forward 800 characters per chunk, so a quarter of the text gets stored twice.

My first document came out as 16 chunks. Working backwards, that means it was roughly 12,800 characters long. Here is what the same document would look like with different overlap settings.

More overlap, more chunks: 13 with none, 16 today, 21 at 400 More overlap, more chunks: 13 with none, 16 today, 21 at 400 A 12,800-character document cut into 1,000-character chunks 0 characters of overlap: 13 chunks 100 characters of overlap: 15 chunks 200 characters of overlap: 16 chunks 300 characters of overlap: 18 chunks 400 characters of overlap: 21 chunks 13 15 16 18 21 my setting 0 100 200 300 400 Overlap between neighbouring chunks, in characters
Chunk count = ceil((length − overlap) ÷ (chunk size − overlap)). 12,800 characters is an estimate of my first document.

And here is what each chunk actually weighs once it is in the database:

WhatPer chunkFirst document (16 chunks)
TextUp to 1,000 characters, about 250 tokensAbout 16,000 characters
Vector768 numbers, about 6 KB as 64-bit floatsAbout 98 KB
Search index768 dimensions, cosine similarityOne index: vector_index

Token figures use the usual rule of thumb of about 4 characters per token for English. The fun fact: the numbers describing my notes weigh about six times more than the notes themselves. Storage is cheap, so I have decided this is fine.

One more catch. The index is welded to the model. nomic-embed-text produces 768 numbers, so the index expects 768. Switch embedding model and you rebuild the index and re-embed every chunk you own.

Workflow 2: asking it questions

A question gets turned into numbers, matched against the stored chunks, and the four closest ones go to Gemini with strict instructions. The first test answered correctly in about 6 seconds.

Every answer is built from the 4 closest chunks Every answer is built from the 4 closest chunks Ask a question n8n chat window Plain English Embed the question Same Ollama model 768 numbers again Find the closest 4 $vectorSearch, rag.notes Ranked by cosine similarity Gemini writes Q&A Chain, temperature 0.2 Only from those 4 chunks Answer in the chat About 6 seconds 1,271 tokens, first test Not in the 4 chunks? The prompt tells Gemini to say it could not find it, not to guess
The chat workflow in n8n: 5 steps, timings from the first test.

Two choices here mattered more than I expected.

  • A Q&A Chain, not an AI Agent. An agent decides for itself whether to search your notes. Sometimes it decides it already knows the answer, which is exactly what you do not want. The chain searches every single time.
  • Temperature 0.2. Boring on purpose. I want the answer from the notes, not a creative reinterpretation of them.

The system prompt is four rules: answer only from the context, say so if the answer is not there, mention the source file, use British English. The last one is non-negotiable.

So where do the tokens go? Mostly on my own notes.

About four-fifths of every answer is my own notes About four-fifths of every answer is my own notes Estimated split of the 1,271 tokens measured on the first test question 4 retrieved chunks 4 retrieved chunks: about 1,000 tokens 1,000 (79%) System prompt System prompt: about 110 tokens 110 (9%) Your question Your question: about 15 tokens 15 (1%) The answer The answer: about 146 tokens 146 (11%) Total: 1,271 tokens. Chunks at roughly 250 tokens each, the rest estimated from text length.
1,271 tokens measured in n8n. The split between parts is estimated.

That matters for two reasons. Cost scales with how many chunks you send, not how clever the question is. And the answer can only be as good as those four chunks. If the right paragraph is ranked fifth, Gemini never sees it, and it will politely tell you it cannot find something that is sitting right there in the database.

I use the paid Gemini tier on purpose. On the free tier, Google can use what you send to improve its products. For my own notes that is a shrug. For a client's price list, it is a conversation I do not want to have.

What broke (spoiler: not the AI)

Twelve things broke. Only two of them had anything to do with AI. The rest were plumbing: the server, the hosting panel, and my own passwords.

Coolify caused 5 of my 12 problems. The AI bits caused 2. Coolify caused 5 of my 12 problems. The AI bits caused 2. 12 problems hit while building, by where they lived. One square per problem, hover for what broke. Coolify 2. Terminal never connected, so no shell 5. Scheduled task commands capped at 255 characters 8. Restart ignores pending changes, Redeploy does not 9. Placeholder wiped the stored mongot.yml 10. Backup volume never actually applied 5 Passwords and keys 4. Keyfile ended in a stray % from zsh 6. An @ in the password broke the connection URL 7. Changing the variable did not change the password 3 Proxmox VM 1. CPU type hid AVX, so MongoDB would not start 1 mongot config 3. Config format changed to a scramAuth block 1 n8n 11. Lost connection after import, chat echoed raw JSON 1 Gemini 12. gemini-2.5-pro retired for new users, 404 1
From my build notes: 12 problems, October 2026.

My personal favourites:

  • The haunted percent sign. I generated a security key on my Mac, copied it, pasted it. zsh had helpfully added a % at the end to say "no newline here". MongoDB looked at it, decided it was not base64, and refused to start. While still showing "Running".
  • The password that was too strong. It contained an @. Connection strings use @ to separate the password from the host name. So MongoDB tried to connect to half my password. Now it is letters and numbers only, like a 1998 Hotmail account.
  • The password that did not change. I changed the password variable in Coolify and expected MongoDB to notice. It did not. The variable is just what the app thinks the password is. The database keeps its own opinion. Locked out of my own database, by my own change.
  • The model that retired. The example used gemini-2.5-pro. Google had retired it for new users, so the first chat answer was a 404. Picked a current Flash model from the dropdown and moved on.

The full list, for anyone about to do the same thing:

#What brokeFix
1MongoDB: "CPU does not support x86-64-v2"Set the Proxmox VM CPU type to host
2No working terminal in CoolifyRun set-up from a start-up script, commands via scheduled tasks
3mongot rejected its configNew scramAuth block in mongot.yml
4Keyfile ended in %Regenerate with ; echo on the end
5Commands over 255 charactersSplit them up
6@ in the password broke the URLPass credentials as flags, simpler passwords
7Changing the variable locked me outChange it inside MongoDB first, then the variable
8"Changes pending" never appliedRedeploy, not Restart
9Stored mongot.yml wiped by a placeholderRestore the file in Persistent Storage
10Backups failed, folder missingStill to do
11Chat echoed raw JSONRedraw the trigger to Q&A Chain connection
12Gemini 404Pick a current model from the dropdown

What I learned

The headline lesson: building a private RAG system is 20% AI and 80% plumbing. The rest of what I learned fits on a sticky note.

  1. Retrieval is the product. Gemini only ever sees four chunks. If search picks the wrong four, the smartest model in the world gives you a confident "not found". Time spent on chunking pays back more than time spent picking models.
  2. Label everything from day one. Every chunk should know which file it came from. Adding it later means deleting everything and re-uploading. Ask me how I know (again).
  3. The embedding model is a marriage, not a date. The index is built for 768 numbers. Change model and you re-embed the lot. That lock-in is one reason plain-text formats like Open Knowledge Format (OKF) are worth a look for smaller, curated knowledge.
  4. Boring beats clever. A chain that always searches beats an agent that sometimes feels like it. Low temperature beats creativity when the job is "read the notes".
  5. Embeddings are cheap, writing is not. A 2015 CPU embeds text happily. Writing good answers is where the paid API earns its keep.
  6. Keep secrets dull. Passwords with only letters and numbers, keys generated with a trailing newline. Fancy characters in secrets are a hobby, not a security policy.
  7. Self-hosting means you are the backup plan. Mine is still "postponed", which is a polite word for "not done". This is the one I would fix first if you are copying me.
  8. It is portable. The search syntax is the same as MongoDB Atlas, so if I ever want someone else to run the database, the workflows do not change.

So, now what?

First, finish the boring bits. Then point it at real documents that someone other than me actually needs to search.

The to-do list before I trust it with anything important:

  • Tag every chunk with its source file, clear the collection, re-upload
  • Publish both n8n workflows so they run without me babysitting the editor
  • Daily mongodump at 3am, keep 7 days, plus a full backup of the VM to external storage
  • Put n8n and Open WebUI behind HTTPS, or take Open WebUI off the internet entirely
  • Smarter chunking that respects headings and paragraphs, and filters by source, date or client

And then the fun part. The real value is not "chat with my notes". It is answering the same questions other people keep asking, from documents that never leave the server:

  • Small business knowledge bases. Handbooks, price lists, specs, warranty terms. The stuff staff ask about ten times a day.
  • A quoting and support assistant for the tint and PPF shop platform: "how much for a full front on a Model 3, and what does the warranty cover?"
  • An assistant for the eSIM sites, answering plan and coverage questions from our own pages.
  • A StoryHawk analyst that can explain what changed in the search results, from our own data.
  • A technical SEO knowledge product, built on years of notes that currently live in my head and in too many tabs.

Want one for your company?

This is the bit where I admit why I wrote all this up. If your team has a shared drive full of handbooks, price lists and policies that nobody can find anything in, this is exactly what I now build for businesses.

You get the same setup as mine, sized for you

  • Your documents stay on your server. Files, chunks and vectors never leave your infrastructure. Only the question and a handful of matching paragraphs go to the answer model, on a paid tier.
  • Open-source and self-hostable, top to bottom. Ollama, MongoDB Community, n8n and Coolify, all running on hardware you control. No per-seat AI licence, and no vendor holding your knowledge hostage. If you ever want to move it, it moves.
  • Answers you can check. Every answer comes from your own documents and names the file it came from. If the answer is not there, it says so instead of making something up.
  • Twelve problems already solved. You get the version after the CPU flags, the haunted percent sign and the password that would not change. With backups from day one, unlike mine.

If that sounds useful, get in touch. Tell me which questions your team answers ten times a day and where the answers live. I will tell you honestly whether you need a private AI knowledge base, or just a better FAQ page.

So, I built a private AI knowledge base on my own server. Now what? Now it stops being a side project and starts being something I build for other people. Right after I set up my own backups.

Svet Petkov, Head of Technical SEO at The Telegraph

Written by

Svet Petkov

Head of Technical SEO at The Telegraph

I'm Svet (Svetoslav) Petkov, an SEO specialist with more than 10 years' experience working in-house and agency-side on medium and large websites. I also have plenty of experience with AI automation, AI search, and building AI-powered apps.