← Back to Blog

Dify Self-Hosted on a 4 GB VPS: RAM per Container, CPU Indexing Speed and a Tested Setup

Published · by Leo Gashi, RS Computers

Dify RAG Self-hosted AI

Dify self-hosted runs on a CPU-only VPS, and its official minimum of 2 CPU cores and 4 GiB of RAM is a real floor, not a comfortable size. We ran the official Docker Compose stack of Dify 1.17.1 on exactly that, 2 vCPUs and 4 GiB with no swap and no GPU, and with its 15 long-running containers idle, the server used 2,257 MB of memory before any model or document was added. Dify is a platform for building AI apps such as chatbots, agents, workflows and RAG search over your own documents, and self-hosting means the whole stack runs on your server.

With Ollama and the nomic-embed-text embedding model added, an 84 KB document was searchable 92 seconds after upload, and ten technical documents (2.2 MB of text) took 42.5 minutes. A 1.5B chat model also fit, but got only some answers right. On our VPS hosting plans, that makes a VPS Mini (4 shared vCPU, 4 GB RAM) the smallest sensible size for Dify, and a VDS Small (4 dedicated vCPU, 8 GB) the starting point for a team that wants local models.

Key facts, checked on 2026-10-17:

Dify self-hosted requirements: the minimum vs what we measured

Dify's Docker Compose guide lists CPU, RAM and software versions. This is how those numbers looked on a running server.

ResourceDify docs minimumMeasured on Dify 1.17.1What we would plan
CPU2 cores2 vCPUs ran it; both were busy (up to 198%) while Ollama embedded documents4 vCPU, dedicated if you run local models
RAM4 GiB2,257 MB idle, 3,172 MB peak while indexing, 3,867 MB with a 0.5B chat model right after indexing4 GB with cloud models, 8 GB or more for local chat
Docker19.03 or later26.1.5 from Debian 13's packagesThe Debian package worked
Docker Compose2.24.0 or later2.26.1 from Debian 13's packages2.24.0 or later; Debian's 2.26.1 also handled the override file below
DiskNo figure given7.98 GB of Dify images, 13.55 GB with Ollama; after tests 257 MB database, 72 MB vectors, 1.6 GB for three modelsVPS Mini's 80 GB NVMe leaves room for documents and more models

A 2 GB server is out: the four Python services alone hold 1.8 GB at idle. Ollama is the big extra on disk, with a 5.57 GB image, so leave it out if you only use cloud models.

How much RAM Dify uses, container by container

Dify is not one program. One to three minutes after creating the admin account, with no plugin or knowledge base yet, we read docker stats --no-stream three times, a minute apart. The table shows the middle reading.

ContainerWhat it doesIdle RAM
worker_beatStarts timed background jobs441 to 503 MiB
apiMain backend: console, apps, API477 MiB
api_websocketLive co-editing of workflows (optional)428 MiB
workerBackground jobs, including indexing426 MiB
webWeb interface (Next.js)124 MiB
sandboxRuns Code nodes in isolation123 MiB
agent_backendRuntime for Dify's newer agent feature113 MiB
db_postgresPostgreSQL 15: accounts, apps, chats, chunk text76 MiB
weaviateVector database for knowledge search69 MiB
agent_ssrf_proxy, ssrf_proxyProxies that block calls to internal addresses32 + 23 MiB
local_sandboxWhere the agent runs commands27 MiB
plugin_daemonRuns plugins, including model providers23 MiB empty, 76 to 271 MiB with the Ollama plugin
redisCache and job queue11 MiB
nginxFront door on ports 80 and 44310 MiB
All 15free -m: 2,257 MB used, 1,838 MB available2,464 MiB

The four Python services at the top each load the whole Dify backend, so they hold 1.8 GB before anyone logs in. They also keep memory they grow into: after our big indexing run the worker peaked at 755 MiB during the next chat test and dropped back to 420 MiB only after we restarted the stack. If Dify shares the server with other apps, our RAM guide for self-hosted apps helps you add things up.

One container is easy to drop. api_websocket only serves live co-editing, and Dify's environment reference gives its browser address, NEXT_PUBLIC_SOCKET_URL, a default of ws://localhost, so on a remote server it needs extra setup before it is any use. With collaboration off, idle use fell from 2,257 MB to 1,862 MB, with Ollama and its plugin installed.

How we tested

In our nested containers, Ollama saw all 8 host cores and ran 8 threads on 2 vCPUs, which made embedding six to seven times slower. A KVM VPS shows its real vCPU count, so we pinned the model to 2 threads.

How long Dify takes to index documents on a CPU

RAG (retrieval-augmented generation) means the app searches your documents and hands the best passages to the language model together with the question. Dify first cuts each document into chunks, and an embedding model turns every chunk into a vector, a list of numbers that describes its meaning. On a CPU, that embedding step is the slow part.

TestChunks and tokensTime until searchablePeak RAM, whole server
RFC 9111 alone (84 KB), High Quality mode454 chunks, 18,620 tokens92 s2,672 MB
All ten (2.2 MB), default worker settings3 of 10 documents finishedStalled after 13 min: 2 errors, 5 stuckNot measured
All ten, CELERY_WORKER_AMOUNT=210,300 chunks, 483,206 tokens42.5 min, about 190 tokens per second3,172 MB
All ten, Economical mode10,300 chunks, no vectors50 s3,256 MB
RFC 9111 uploaded again454 chunks4 sNot measured separately

Dify reuses stored embeddings for identical text, hence the 4-second re-upload; we emptied that cache before the timed runs. Ollama peaked at 198% CPU, so both vCPUs were busy, and a search took 1.0 to 2.8 seconds because each question is embedded on the CPU too.

Economical mode stores 10 keywords per chunk instead of vectors. It indexed 51 times faster but found far less: for three questions with clear answers, High Quality returned a passage from the right RFC every time, Economical once. A High Quality knowledge base cannot be switched back to Economical later (Dify docs on index methods).

Why a batch upload stalled, and the one-line fix

Dify's worker is a Celery process, a Python job queue, and CELERY_WORKER_AMOUNT sets how many documents it indexes at once: 4 by default. Each document gets 10 embedding threads (max_workers = 10 in Dify's indexing_runner.py), and every thread holds a database connection while it waits for the model.

Ollama answers one request at a time (OLLAMA_NUM_PARALLEL defaults to 1, per the Ollama FAQ). On 2 cores, 40 threads waited on 40 connections, exactly the default pool of 30 plus 10 overflow. Two documents and a scheduled job failed:

# from the worker log (docker compose logs worker)
QueuePool limit of size 30 overflow 10 reached, connection timed out, timeout 30.00

Five more documents stayed on "waiting" or "indexing" for good, with no error shown. The fix is one line in .env:

# as the dify user, in ~/dify/docker
sed -i 's/^CELERY_WORKER_AMOUNT=.*/CELERY_WORKER_AMOUNT=2/' .env
docker compose up -d worker

Dify's environment reference already says to lower this "on constrained VMs". With 2, all ten files indexed without an error, and single documents took no longer than in the stalled run (RFC 7208 about 7 minutes both times), because Ollama was the queue anyway. Re-upload any documents that failed before.

Why ten files became 10,300 chunks

Dify's general chunking cuts at every delimiter and never joins short pieces (we checked fixed_text_splitter.py), so every RFC paragraph became a chunk of 47 tokens on average. That hurts answers. Top K, the number of passages Dify hands to the model, is 3 by default. For "What is the maximum number of DNS lookups during an SPF check?" the passage with the answer (10) ranked fifth, and the first hit was the bare heading "4.6.4. DNS Lookup Limits".

For text with short paragraphs, try a rarer delimiter or parent-child mode (see the chunking guide), and test with real questions.

Can Dify run a local chat model in 4 GB without a GPU?

A small one, just. We built a chat app on the ten-RFC knowledge base with two Qwen 2.5 models and asked three questions: the status code for a request that is too large (413), how long an SMTP client waits for the first 220 greeting (5 minutes), and the SPF lookup limit (10).

Model and setupPeak RAM, whole serverTime to first wordAnswers
qwen2.5:0.5b (397 MB), stock settings, right after the 42-minute indexing run3,867 MB of 4,09634.8 s cold, 5.8 to 11.8 s warm413 right; 5 minutes, wrong reason; SPF wrong ("two")
qwen2.5:0.5b after a restart, collaboration off2,506 MB17.4 s cold, 3.4 to 7.6 s warmThe same three answers
qwen2.5:1.5b (986 MB), collaboration off, set as default model3,246 MB (3,714 MB while the 0.5B model was also loaded)5.1 to 27.4 sFirst three: 413 and 5 minutes right, SPF wrong; three new questions: 1 right, 1 close, 1 wrong

Once the first word appeared, the 1.5B model wrote about 10 tokens per second and the 0.5B model up to 26. Asked in the RFC's own words ("DNS-querying terms"), the 1.5B model answered 10, so retrieval lost the SPF question, not the model.

One trap: Dify titles each new conversation with the workspace's default model, which is the first LLM you add. Ours was still the 0.5B model, so each chat loaded two models and one answer took 67 seconds. Set the default to your app's model.

On 4 GB, local chat is a demo. Use a cloud model there, or a VDS plan with 8 GB or more for a 3B model. Our guide to running 3B to 8B models on a CPU server lists RAM and speed per model size. A small local model also ran in our Odysseus AI workspace test on CPU.

Which server size and city for Dify

Your situationPlanCityWhy
Dify with cloud models, or local embeddings for a modest knowledge baseVPS Mini: 4 shared vCPU, 4 GB RAM, 80 GB NVMeThe one nearest your users2.3 GB idle and 3.2 GB peak while indexing in our test on 2 vCPUs; Mini has twice the vCPUs
A team using local embeddings plus a 1.5B to 3B chat modelVDS Small: 4 dedicated vCPU, 8 GB, 240 GB NVMeThe one nearest your teamRoom for the chat model next to Dify; dedicated vCPUs keep indexing times steady
Large document sets, several apps, a 7B to 8B local modelVDS Medium: 8 dedicated vCPU, 16 GB, 480 GB NVMeThe one nearest your usersTwice the cores for embedding and answers, memory for a 5 GB model
Company documents that must stay in the EUAny of the three aboveAmsterdam or DublinBoth cities are in the EU, where the GDPR applies directly
Users in Kosovo, Albania or North MacedoniaAny of the three abovePrishtinaClosest to the Balkans; Skopje is about 3 ms away on our own network

VPS Nano (1 GB) and VPS Micro (2 GB) are smaller than Dify's idle stack, so we do not suggest them for Dify. In our ping.pe measurements of 6 October 2026, published on our location pages, the Amsterdam server answered Frankfurt in 6.6 ms and London in 6.7 ms, and the Prishtina server answered Vienna in 18.6 ms and Frankfurt in 27.8 ms.

Every plan has NVMe, unmetered traffic and its own IPv4 and IPv6, and an upgrade from the client area needs only a short reboot. Availability by city is on the plans page.

How to self-host Dify with Docker Compose, step by step

Docker Compose starts a set of containers from one file, and Dify ships that file. These are our lab steps on Debian 13. Start from a server prepared as in our Debian 13 security guide; ports published by Docker skip ufw rules, as our Docker on a VPS guide explains.

Step 1. Install Docker, Compose, git and jq, and create a user for Dify:

# as root
apt update
apt install -y docker.io docker-compose git curl jq
adduser --disabled-password --gecos "" dify
usermod -aG docker dify
docker --version
docker compose version

We saw Docker version 26.1.5+dfsg1 and Docker Compose version 2.26.1-4. Members of the docker group control every container, so treat the dify user like root.

Step 2. Download a fixed version:

# as the dify user (su - dify)
git clone --depth 1 --branch 1.17.1 https://github.com/langgenius/dify.git
cd dify/docker

Step 3. Set your own secrets before the first start, because PostgreSQL keeps the password it was created with. The example file ships with shared defaults such as difyai123456. This script replaces them all and sets the worker fix; its last line should print 0:

# as the dify user, in ~/dify/docker
cp .env.example .env
new() { openssl rand -hex 24; }
DBP=$(new); RP=$(new); WK=$(new); SBK=$(new)
AGENTKEY=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')
sed -i \
  -e "s|^SECRET_KEY=.*|SECRET_KEY=$(openssl rand -base64 42)|" \
  -e "s|^INIT_PASSWORD=.*|INIT_PASSWORD=$(openssl rand -hex 12)|" \
  -e "s|^DB_PASSWORD=.*|DB_PASSWORD=$DBP|" \
  -e "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=$RP|" \
  -e "s|^CELERY_BROKER_URL=.*|CELERY_BROKER_URL=redis://:$RP@redis:6379/1|" \
  -e "s|^WEAVIATE_API_KEY=.*|WEAVIATE_API_KEY=$WK|" \
  -e "s|^WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS=.*|WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS=$WK|" \
  -e "s|^CODE_EXECUTION_API_KEY=.*|CODE_EXECUTION_API_KEY=$SBK|" \
  -e "s|^SANDBOX_API_KEY=.*|SANDBOX_API_KEY=$SBK|" \
  -e "s|^PLUGIN_DAEMON_KEY=.*|PLUGIN_DAEMON_KEY=$(new)|" \
  -e "s|^PLUGIN_DIFY_INNER_API_KEY=.*|PLUGIN_DIFY_INNER_API_KEY=$(new)|" \
  -e "s|^DIFY_AGENT_API_TOKEN=.*|DIFY_AGENT_API_TOKEN=$(new)|" \
  -e "s|^DIFY_AGENT_SERVER_SECRET_KEY=.*|DIFY_AGENT_SERVER_SECRET_KEY=$AGENTKEY|" \
  -e "s|^CELERY_WORKER_AMOUNT=.*|CELERY_WORKER_AMOUNT=2|" \
  .env
chmod 600 .env
grep -c "difyai123456\|dify-sandbox\|dev-only\|WVF5YTha\|lYkiYYT6\|QaHbTe77\|MDEyMzQ1" .env

The Redis password also sits inside CELERY_BROKER_URL, and the Weaviate and sandbox keys appear twice, so one value serves each pair. The agent key is made differently because DIFY_AGENT_SERVER_SECRET_KEY must decode to exactly 32 bytes; a 48-character hex string broke our first attempt.

Step 4. The plugin daemon publishes port 5003 (remote plugin debugging) on every interface. Setting EXPOSE_PLUGIN_DEBUGGING_PORT=127.0.0.1:5003 in .env makes the api container restart in a loop, so use an override file next to the main one instead; Compose reads it automatically. Ours keeps port 5003 local and adds Ollama without publishing its port:

# ~/dify/docker/docker-compose.override.yaml, owned by the dify user
services:
  plugin_daemon:
    ports: !override
      - "127.0.0.1:5003:5003"
  ollama:
    image: ollama/ollama:0.40.6
    restart: always
    volumes:
      - ./volumes/ollama:/root/.ollama

!override replaces the port list instead of adding to it (Compose merge rules); Debian's Compose 2.26.1 accepted it, and docker compose config showed the port bound to 127.0.0.1. Leave out ollama if you only use cloud models.

Step 5. Start everything:

# as the dify user, in ~/dify/docker
docker compose up -d
docker compose ps

The first image pull took 115 seconds for us, and the API answered 110 seconds after up -d. The api container runs its database migrations at start (MIGRATION_ENABLED=true in .env), and until it is ready, API calls return a 502 error.

Every service should read Up, and init_permissions Exited (0). In PORTS, nginx shows 0.0.0.0:80->80/tcp and 0.0.0.0:443->443/tcp, plugin_daemon 127.0.0.1:5003->5003/tcp, and ollama only 11434/tcp, meaning not published.

Step 6. Claim the admin account at once: whoever opens /install first becomes the owner, hence INIT_PASSWORD. Read it:

# as the dify user, in ~/dify/docker
grep ^INIT_PASSWORD= .env

Open http://YOUR_SERVER_IP/install, enter it, and create the admin with a password of at least 8 characters with letters and numbers. Through Dify's console API, creating the admin without the init password returned 401 Unauthorized, and with it success. Before real use, add a reverse proxy with a TLS certificate.

Connect Ollama to Dify for local embeddings

Ollama runs open models on your own server, and Dify talks to it through a plugin. Pull the embedding model first:

# as the dify user, in ~/dify/docker
docker compose exec ollama ollama pull nomic-embed-text
docker compose exec ollama ollama list

The list should show nomic-embed-text:latest at 274 MB. After the first request, docker compose logs ollama | grep n_threads shows how many threads Ollama uses. Only if that differs from your vCPU count, pin it (2 here):

# as the dify user, in ~/dify/docker
printf 'FROM nomic-embed-text\nPARAMETER num_thread 2\n' > Modelfile.embed
docker compose cp Modelfile.embed ollama:/tmp/Modelfile.embed
docker compose exec ollama ollama create nomic-embed-text -f /tmp/Modelfile.embed

In Dify, install the Ollama plugin by langgenius from the Marketplace. Then add a model under Ollama in the model provider settings: name nomic-embed-text, type Text Embedding, base URL http://ollama:11434 without /api, context size 2048. We did both through Dify's console API, which these forms call: the plugin installed in 8 seconds, and the model saved in 16 seconds, including its first load.

Then create a High Quality knowledge base with that model. Because Dify reaches Ollama by service name, you skip OLLAMA_HOST=0.0.0.0 and an open Ollama API with no password.

Make a small Dify server lighter

This turns off collaboration mode and three outside calls. Weaviate reports its version, host system, modules and object count every 24 hours, and its docs say the data may be used "to identify potential customers" (Weaviate telemetry). Dify's compose file also sets NEXT_TELEMETRY_DISABLED to 0 for the web container, and the console checks updates.dify.ai for new versions.

Run down first: once collaboration leaves COMPOSE_PROFILES, Compose stops seeing that container and leaves it running, as it did for us.

# as the dify user, in ~/dify/docker
docker compose down
sed -i \
  -e 's/^COMPOSE_PROFILES=.*/COMPOSE_PROFILES=${VECTOR_STORE:-weaviate},${DB_TYPE:-postgresql}/' \
  -e 's/^ENABLE_COLLABORATION_MODE=.*/ENABLE_COLLABORATION_MODE=false/' \
  -e 's/^WEAVIATE_DISABLE_TELEMETRY=.*/WEAVIATE_DISABLE_TELEMETRY=true/' \
  -e 's/^CHECK_UPDATE_URL=.*/CHECK_UPDATE_URL=/' \
  .env
echo 'NEXT_TELEMETRY_DISABLED=1' >> .env
docker compose up -d

Afterwards docker compose ps listed 15 services with Ollama and no api_websocket, and apps answered as before. The Marketplace stays on for plugins.

Troubleshooting self-hosted Dify: the errors we hit

Every row below happened in our lab on Dify 1.17.1. docker compose logs followed by the service name shows the error text.

What you seeCauseFix
agent_backend restarts in a loop with must decode to exactly 32 decoded bytesDIFY_AGENT_SERVER_SECRET_KEY has the wrong length, such as 48 hex charactersGenerate it from 32 random bytes, as the script in step 3 does
api restarts with PLUGIN_REMOTE_INSTALL_PORT must be a bare port numberEXPOSE_PLUGIN_DEBUGGING_PORT was set to 127.0.0.1:5003Keep the value from .env.example and bind the port with the override file in step 4
API calls return 502 in the first minutesThe api container is still running database migrationsWait; ours answered 110 seconds after up -d
init_permissions shows Exited (0)A one-time task that sets file permissionsNothing, this is expected
Documents stay on "waiting" or "indexing", or fail with QueuePool limit of size 30 overflow 10 reached4 documents at once with 10 threads each fill the 40 database connections while CPU embedding is slowCELERY_WORKER_AMOUNT=2, then re-upload the failed documents
api_websocket still runs after you turned collaboration offcollaboration left COMPOSE_PROFILES before docker compose downdocker compose --profile collaboration rm -sf api_websocket
Each chat loads a second model, and one answer took 67 secondsDify names conversations with the workspace's default modelSet the default model to your app's model
The answer is in your documents, but the reply is wrongShort chunks pushed the right passage below the top 3 that reach the modelTry a rarer delimiter or parent-child chunks, and test with real questions

Where self-hosted Dify stores your documents (GDPR)

Everything sits in ~/dify/docker/volumes: PostgreSQL with accounts, apps, chats and the text of every chunk, Weaviate's vectors, uploads and plugins. After our tests the database took 257 MB and Weaviate 72 MB.

With Ollama, prompts and documents stay on the server; with a cloud model, questions and retrieved passages go to the provider. When you copy volumes for safekeeping, copy .env with it, because it holds the database password and the other keys the stack was created with. The keys that decrypt saved model API credentials sit in volumes/app/storage/privkeys, per Dify's source, so keep the folder whole. Amsterdam and Dublin keep documents in the EU, and our CLOUD Act checklist for European hosting lists what a data protection officer will ask.

A fresh 1.17.1 install needs nothing here. For an older install updated in place, the release notes warn that pulling and restarting "can silently and permanently break vector search". Dify's Weaviate upgrade path lists 13 steps, one minor version at a time, after a copy of ./volumes/weaviate; we did not run it.

After every update, compare .env.example with your .env: 1.16.1, for example, added the required DIFY_AGENT_API_TOKEN.

Frequently asked questions

What are the minimum requirements for Dify self-hosted?

Dify's docs ask for at least 2 CPU cores, 4 GiB of RAM, Docker 19.03 and Docker Compose 2.24.0 on Linux. In our test of Dify 1.17.1 on exactly 2 vCPUs and 4 GiB, the idle stack used 2,257 MB, leaving about 1.8 GB for plugins, models and indexing. Dify's images took 7.98 GB of disk, or 13.55 GB with Ollama.

How much RAM does Dify use?

The official Docker Compose stack of Dify 1.17.1 keeps 15 containers running and used 2,257 MB at idle in our lab, about 1.8 GB of it in four Python services. Indexing ten documents with local embeddings peaked at 3,172 MB. With collaboration mode off, idle use fell to 1,862 MB, with Ollama and its plugin installed.

Can Dify run on a 2 GB VPS?

No. In our test of Dify 1.17.1, the api, worker, worker_beat and api_websocket services alone held about 1.8 GB before anyone logged in, and the whole idle stack used 2,257 MB. Plan 4 GB as the floor, and 8 GB or more if you also want a local chat model.

Can Dify run without a GPU?

Yes. Dify needs no GPU and can use cloud models through an API. With Ollama on 2 vCPUs, nomic-embed-text indexed 483,206 tokens in 42.5 minutes. A 1.5B chat model ran in 4 GB but answered only some test questions correctly, so plan 8 GB or more for local chat.

How long does Dify take to index documents on a CPU?

On 2 vCPUs with nomic-embed-text, one 84 KB document took 92 seconds and ten documents with 2.2 MB of text took 42.5 minutes, about 190 tokens per second. Set CELERY_WORKER_AMOUNT=2 first: with the default of 4, the batch hit database pool timeouts and only 3 of 10 documents finished.

Is self-hosted Dify free for commercial use?

Mostly. The licence is Apache 2.0 with two conditions: a multi-tenant service, where one tenant is one workspace, needs Dify's written permission, and the logo and copyright in the frontend must stay. Using Dify inside your company or behind your own product is allowed.

Start with the stack, add models later

A 4 GB server runs Dify well enough to build apps, index a few hundred pages and answer with a cloud model, once the worker is set to 2. Local chat models want 8 GB and dedicated cores. RS Computers runs KVM VPS and VDS servers in Amsterdam, Dublin and Prishtina: pick a size and a city on our VPS hosting plans page, or message us on Telegram for help sizing a server.

← All articles

Chat on Telegram