Back to blog

Running a Vector Database on a NAS

5 min read Tools & Automation

I have a personal knowledge base. Over a thousand course transcripts, chunked and embedded, stored in a vector database so I can search them with natural language. Ask a question, get relevant answers from any course I’ve ever taken.

Most people running vector databases use a hosted service. Pinecone, Weaviate Cloud, whatever. They charge by the number of vectors you store and the queries you run. For a personal project with millions of vectors, the monthly bill adds up.

I already had a Synology DS1520+ NAS sitting on my desk. It runs 24/7, has spare CPU and RAM, and stores all my files anyway. So I put the database there.

The Setup

Hardware: Synology DS1520+ (Intel Celeron J4125, 8GB RAM)

Database: Qdrant, an open-source vector database. Fast, lightweight, and has a good web dashboard for inspecting collections.

Container runtime: Docker, managed through Portainer’s web UI. Portainer gives you a clean interface for managing containers, volumes, and stack deployments without SSH-ing into the NAS.

The whole thing runs behind Cloudflare Tunnel, so I can query it from anywhere without exposing ports on my home network.

Why Qdrant

I evaluated a few options before settling on Qdrant.

ChromaDB is popular in the Python/LLM ecosystem. But it’s designed for development and prototyping, not production use. No built-in authentication. Limited web UI.

Milvus is more enterprise-grade but heavier. Needs more resources than my NAS has to spare. Overkill for a personal knowledge base.

Qdrant hit the sweet spot. Lightweight enough to run on a NAS. Good REST API. Nice web dashboard. Built-in authentication. Active development. And it handles collections with millions of vectors without breaking a sweat.

Docker on Synology

Synology’s Docker support comes through the Container Manager package (it was called Docker before DSM 7.2). You can install it from the package center, but I prefer Portainer for managing containers.

The Portainer stack is straightforward. A docker-compose.yml file that defines the Qdrant service, mounts the storage volume, and exposes the HTTP and gRPC ports. I keep the data in /volume1/docker/qdrant so it survives container updates and NAS reboots.

One important detail: the API key. Qdrant supports setting an API key via the QDRANT__SERVICE__API_KEY environment variable. Without it, anyone on the network can read and write to your collections. With it, every request needs to include the key. I added it to the stack configuration and restarted the container.

The API key also locks down the web dashboard. When you open the Qdrant UI, it prompts for credentials before showing anything. Browsers may cache the session, so if you make changes, test in an incognito window to verify the key is being enforced.

Collections

I split my data into two collections:

  • course-transcripts: Business, marketing, and course creation content
  • drone-courses: FAA Part 107 drone training material

Separate collections because the content domains are completely different. When I search for “obstacle clearance requirements,” I want drone course results, not marketing advice. Collections give me namespace isolation without the complexity of metadata filtering.

Each collection uses the all-MiniLM-L6-v2 embedding model (384-dimensional vectors, cosine similarity). It’s fast, small, and accurate enough for semantic search on English text. The model runs locally during upload via FastEmbed. No API calls to OpenAI or anyone else.

The Upload Pipeline

The upload script scans a directory of transcript files, extracts text from each one (txt, pdf, docx, srt, vtt, epub, rtf), chunks it into ~500 token segments with 50 token overlap, generates embeddings, and upserts everything to Qdrant in batches.

Key features that saved me:

Skip check. Before uploading, the script checks what’s already in the collection by filename. If a file exists, it skips it. This makes the process resumable. You can stop and restart without re-uploading everything.

Confirmation prompts. The script asks for confirmation before uploading and again before creating a new collection. Prevents accidental overwrites.

Chunk deduplication via deterministic IDs. Each chunk gets a UUID generated from the collection name, filename, and chunk index. Same inputs always produce the same ID. This means re-uploading the same file doesn’t create duplicates. It upserts, replacing the old data.

I ran the upload from my Mac, pointing at transcript directories on external drives. The script talks to Qdrant on the NAS over the local network. No data leaves my house.

Lessons Learned

Backup before bulk operations. I wrote a deduplication script that checked for duplicate filenames. It found 25,666 “duplicates” and deleted them. The problem: dozens of courses have files named “01-Introduction.mp4.” Same filename, different content. The backup I’d exported the week before saved me. I wiped the collection and started over.

API key enforcement depends on how you connect. If you access Qdrant through localhost port mapping (which is how Docker on NAS works), the API key may not be enforced on the HTTP port. Test from a different machine to verify.

Multi-column PDFs need special handling. Standard PDF text extraction reads columns top-to-bottom, producing garbled output on two-column layouts. Using pdfplumber with layout=True fixes this by reconstructing the spatial reading order.

What It Enables

Having the database on the NAS means it’s always available. Any machine on my network can query it. I use it through Craft Agents, which connects to Qdrant via an MCP source. I ask questions in plain language and get back relevant chunks from courses I’ve taken.

The whole stack (NAS, Docker, Qdrant, upload scripts, MCP integration) costs zero per month. No subscriptions, no API fees, no cloud provider lock-in. Just hardware I already owned and open-source software.

If you’re building systems to organize and leverage your knowledge, let’s talk.

Share

More writing