Why self-host PrivateGPT on a VPS
PrivateGPT is built around RAG (Retrieval-Augmented Generation): it ingests your documents, splits them into chunks, computes embeddings stored in a vector database (Qdrant by default, in local disk mode), then answers questions from that corpus while citing the passages it used. Since version 1.0, released in June 2026, PrivateGPT no longer runs models itself: it connects to an OpenAI-compatible inference server (Ollama, llama.cpp or vLLM) that you install alongside it on the same VPS. The LLM, the embeddings and the index therefore stay on your machine, and no request goes out to an AI provider.
On a dedicated VPS, you get a company document assistant isolated from individual workstations, with a centralized knowledge base the whole team can query, without ever exposing contracts, HR files or legal documents to a cloud service. There is a legal angle too: pasting a document containing personal data into a hosted AI API makes that provider a processor under the GDPR, and the processing must be governed by a contract (Article 28(3)). Processing that stays on your own server does not add that link to the chain.
Concrete benefits of self-hosting
- Full confidentiality: documents, embeddings and the Qdrant index never leave the VPS
- Sourced answers: every reply relies on passages retrieved from your files, not on the model's general memory
- No API costs when the model runs locally through Ollama or llama.cpp
- Ingestion of PDF, DOCX, PPTX, EPUB, CSV, Markdown,
.mboxmail archives and even images or audio, organized into collections - A Messages-style API (modeled on the Anthropic API specification) to plug PrivateGPT into your internal tools
- Easier compliance for sensitive data (GDPR, professional secrecy): no additional AI processor
PrivateGPT and Ollama: two complementary roles
The two tools are often confused, yet they sit at two different layers of the stack. Ollama is a model server: it downloads LLMs and embedding models, loads them into memory and exposes an API on port 11434. It knows nothing about your files: it answers from whatever context it is sent.
PrivateGPT (repository zylon-ai/private-gpt, Apache-2.0 license) is the RAG application layer: it indexes your documents, retrieves the relevant passages for each question and injects them into the context sent to Ollama. In 1.0 it therefore never works alone: without a reachable inference server, it starts with no models at all.
PrivateGPT vs Ollama: who does what
Scroll the table
| Criterion | PrivateGPT 1.0 | Ollama |
|---|---|---|
| Role | RAG application: ingestion, search, cited answers | Inference server: loads and runs the models |
| Runs the model | No: delegates to an OpenAI-compatible server | Yes, on CPU or GPU |
| Reads your files | Yes: PDF, DOCX, CSV, `.mbox`… by collection | No: context supplied by hand |
| Vector database | Qdrant (local disk by default) | None |
| Interface | Workbench at `/ui`, no access control | REST API, no server-side web interface |
| Default port | `8080` | `11434`, bound to `127.0.0.1` |
Hardware and software requirements
Memory is sized for the model Ollama serves, not for PrivateGPT itself. The models suggested by default in the documentation target a GPU machine (qwen3.5:35b, about 24 GB to download): on a CPU VPS, pick a lighter variant from the same family, qwen3.5:4b (3.3 to 4 GB) or qwen3.5:9b (6.6 to 7.6 GB), plus mxbai-embed-large (about 670 MB) for embeddings. Plan 8 GB of RAM for the 4b variant and 16 GB for the 9b, to leave room for the system, Qdrant and documents being ingested, and at least 4 vCPUs: on CPU, core count sets generation speed.
Allow 30 GB of disk, ideally NVMe, for models, the Qdrant index and ingested documents: ingesting a large PDF reads and writes a lot. On the software side: Docker (or Python 3.11 and uv if you install the package outside a container), Ollama, and a domain pointing to the VPS to expose the interface behind a reverse proxy.
Pre-installation checklist
- A VPS with at least 8 GB of RAM (16 GB for a 9-billion-parameter model) and 4 vCPUs
- Up-to-date Debian 12 or Ubuntu 24.04, SSH access with
sudo - Docker installed (
docker versionresponds) or, failing that,uvwith Python 3.11 - 30 GB of free disk, NVMe preferred, for models and
local_data/ - A subdomain (for example
docs.your-domain.com) pointing to the VPS IP - An active firewall: only SSH, 80 and 443 open, ports
8080and11434stay closed
Deploying PrivateGPT in local mode
Install Ollama and pull the models
Install Ollama with the official script, then download a CPU-friendly LLM and the recommended embedding model:
curl -fsSL https://ollama.com/install.sh | sh ollama pull qwen3.5:4b ollama pull mxbai-embed-largeBy default Ollama only listens on
127.0.0.1:11434: keep it that way, only PrivateGPT needs to reach it. This is the only step that needs Internet access; once the models are in place, inference runs offline.Run PrivateGPT with Docker
The official
zylonai/private-gptimage ships thecorepackage. On Linux the container must share the host network to reach Ollama onlocalhost, and a volume must keep the data, otherwise ingested documents vanish on restart:mkdir -p /opt/privategpt/local_data docker run -d --name privategpt --restart unless-stopped \ --network host \ -e OPENAI_API_BASE=http://localhost:11434/v1 \ -v /opt/privategpt/local_data:/home/worker/app/local_data \ zylonai/private-gpt:latestAt startup, PrivateGPT calls
GET /v1/modelson Ollama and registers the models it finds automatically: no configuration file is required. TheOPENAI_EMBEDDING_API_BASEvariable is only needed if your embeddings are served elsewhere.Ingest your documents by collection
Send your files to the ingestion API, grouping them into collections, for example one collection per client file or per department:
curl -X POST http://localhost:8080/v1/artifacts/ingest \ -F "file=@/srv/documents/contrat-cession.pdf" \ -F "collection=legal" curl "http://localhost:8080/v1/artifacts/list?collection=legal"PrivateGPT splits, vectorizes and indexes each file in Qdrant. Formats without a dedicated reader are read as plain text.
Query the corpus with citations
Test first in the Workbench (
http://localhost:8080/ui), then plug your tools into the Messages API. Document search goes through the built-insemantic_search_v1tool, restricted to one collection:curl http://localhost:8080/v1/messages \ -H "Content-Type: application/json" \ -d '{"model": "qwen3.5:4b", "messages": [{"role": "user", "content": "What are the conditions precedent in the contract?"}], "tools": [{"name": "search_docs", "type": "semantic_search_v1", "context": [{"type": "ingested_artifact", "context_filter": {"collection": "legal"}}]}]}'Set up the reverse proxy, SSL and authentication
The Workbench has no access control: the documentation reserves it for local use. Expose it only behind Nginx or Caddy, with a Let's Encrypt certificate and basic authentication:
server { listen 443 ssl; server_name docs.your-domain.com; ssl_certificate /etc/letsencrypt/live/docs.your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/docs.your-domain.com/privkey.pem; auth_basic "Restricted access"; auth_basic_user_file /etc/nginx/.htpasswd; location / { proxy_pass http://127.0.0.1:8080; } }If only two or three people use it, an SSH tunnel (
ssh -L 8080:127.0.0.1:8080 user@your-vps) avoids any public exposure at all.Lock down access
PrivateGPT listens on
0.0.0.0by default, so host networking publishes port8080on every interface. Close it at the firewall so that only the reverse proxy is reachable:ufw default deny incoming ufw allow OpenSSH ufw allow 80,443/tcp ufw enableYour users then go through
https://docs.your-domain.comand query the corpus in natural language.
Ollama cannot report its models' context window, so PrivateGPT only estimates tokens roughly: on long documents, answers may come back truncated. Declare context_window explicitly in a model profile (a settings-<name>.yaml file, activated with PGPT_PROFILES), with a value well below the model's real limit. Also settle on your embedding model from the start: changing it later triggers an "Embedding dimensions mismatch" error and forces you to re-index the whole corpus (embed_dim is 1024 for mxbai-embed-large).
Back up and verify that no data leaves
What to back up. PrivateGPT's full state lives in /opt/privategpt/local_data (Qdrant index under local_data/qdrant, ingested files), plus your source documents and any settings-*.yaml profiles. Ollama models can be downloaded again, so there is no point backing them up. Stop the container while copying (docker stop privategpt), or copy a snapshot of the volume: an index copied during an ingestion may be inconsistent. For several instances or a large team, the documentation recommends a separate Qdrant server rather than local disk mode.
Check outbound traffic. Confidentiality is a property of the architecture, and it can be verified. While running a series of questions, list established connections: only PrivateGPT's connections to 127.0.0.1:11434, the reverse proxy's and your SSH session should appear.
ss -tnp state establishedAny unexpected outbound connection deserves an explanation before you trust the tool with sensitive documents.
Troubleshooting: the most common errors
PrivateGPT starts with no models. Ollama was not running when the container started, or OPENAI_API_BASE is wrong: the URL must end with /v1. Check with curl http://localhost:11434/v1/models, then restart the container (docker restart privategpt).
"Connection refused" from the container. In a bridge-network container, localhost means the container itself. On Linux keep --network host; on macOS or Windows, target host.docker.internal.
"Embedding dimensions mismatch" during ingestion. The index was built with a different embedding model. Switch back to the original model, or wipe the vector store (make wipe in the repository) and re-ingest the documents.
Truncated answers on long documents. The context window is misestimated: declare context_window in a model profile (see the tip above).
Ollama killed mid-answer. The kernel ran out of memory (OOM): dmesg | grep -i oom confirms it. Switch to a smaller variant of the model or add RAM.
"Address already in use" at startup. Port 8080 is already taken: in Docker, set the PORT variable (for example -e PORT=8090), outside a container use the --port option of private-gpt serve, then adjust the Nginx proxy_pass.