What the stack runs and what it needs
The Docker Compose deployment starts seven core services (api, api_websocket, worker, worker_beat, web, plugin_daemon, agent_backend), eight dependencies (Weaviate for vectors, PostgreSQL, Redis, nginx, two SSRF proxies and two sandboxes) and init_permissions, a one-time task that sets storage ownership and exits. All of it runs on the CPU. Dify does not run models itself; it calls the providers you install, whether hosted APIs or a local engine.
| Item | Figure | Scope and source |
|---|---|---|
| Dify stack | 2 CPU cores, 4 GiB RAM | Minimum system requirements, Dify docs |
| Docker Compose | 2.24.0 or later | Required by Dify; the !override tag used below needs 2.24.4, per Docker's Compose merge reference |
| Docker Engine | 28 or later | Our recommendation: older engines let hosts on the same segment reach ports published on 127.0.0.1 (moby#45610) |
| Local 7-8B model at 4-bit | about 5 GB of GPU memory | Weights only, from the table on our GPU page |
| Local 70B model at 4-bit | about 38 GB of GPU memory | Weights only; context and runtime need head-room on top |
The docs give no figure for knowledge bases, so size memory and disk by what you plan to index. Uploads land in docker/volumes/app/storage, vectors in docker/volumes/weaviate, and the metadata database in docker/volumes/db.
Which machine, and why a whole one
If every model you call is a hosted API, Dify is a CPU workload and any x86 line on the dedicated server range that clears the minimum will carry it. Not the X-Gene ARM cartridge: it boots only Ubuntu 16.04 and 18.04, too old for current Docker. If the models stay on your hardware, the GPU dedicated servers put the card in a physical slot handed directly to your OS, with no hypervisor, vGPU slice or time-slicing between them. You install and pin the CUDA or ROCm release your model runtime was tested against, and nothing upgrades it under you.
Everything else in the chassis is yours on the same terms: every core, all the memory, both disks, root, and the server's own iLO or iDRAC console on a separate management network, which still shows the screen if a firewall rule cuts SSH. The port is unmetered, so pulling a 43 GB model file or re-indexing a document set adds nothing to the bill. LLM hosting works out which model fits which build. A racked machine is live about 30 minutes after the order is approved; a build to order takes 4-24 hours.
Ports the stock Compose file publishes
Out of the box, docker-compose.yaml maps nginx to host ports 80 and 443, plus the plugin daemon's remote-debugging port to 5003, each with no address. A port mapping with no address lands on every host address, 0.0.0.0 and [::] alike. Published traffic skips ufw's rules on IPv4, and on IPv6 only when Docker's own IPv6 networking is enabled; otherwise the [::] side is handled by docker-proxy, a process that ufw does filter. Do not lean on the firewall for these three.
Dify's .gitignore already reserves docker/docker-compose.override.yaml for local changes, and Compose reads that file without being told. The override in the setup rebinds nginx to 127.0.0.1:8080 and the debugging port to 127.0.0.1:5003, IPv4 loopback only, so nothing listens on [::] at all and the setup wizard is reachable only through SSH.
Setting it up
Ubuntu, as root (sudo -i), with Docker Engine and the Compose plugin from Docker's apt repository. ufw goes on before anything listens. IPV6=yes is set first because ufw writes IPv6 copies only of rules added while it is set. The clone line is the docs' own: it asks the GitHub API for the latest release tag.
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw default deny incoming
ufw allow 22/tcp
ufw --force enable
apt-get update && apt-get install -y git curl jq
docker version
docker compose version
git clone --branch "$(curl -s https://api.github.com/repos/langgenius/dify/releases/latest | jq -r .tag_name)" https://github.com/langgenius/dify.git /opt/dify
cd /opt/dify/docker
cp .env.example .env
chmod 600 .env
If the clone stops with Remote branch null not found, the unauthenticated GitHub API call was rate-limited; the docs' fix is to pass the version from the releases page to --branch by hand. Next, before the first start, replace every development default in .env. Per the docs, SECRET_KEY signs sessions and tokens and encrypts OAuth credentials, the agent backend refuses a DIFY_AGENT_SERVER_SECRET_KEY that is not unpadded base64url decoding to 32 bytes, and .env.example ships fixed database and Redis passwords plus sandbox, plugin daemon, Weaviate and agent keys. The script generates each value inside Python and writes it into the file, so no secret reaches a command line, ps or shell history.
python3 - <<'PY'
import re, secrets
t = lambda: secrets.token_urlsafe(32)
redis, sandbox, weaviate = secrets.token_hex(24), t(), t()
new = {'SECRET_KEY': secrets.token_urlsafe(42), 'INIT_PASSWORD': secrets.token_urlsafe(18),
'DB_PASSWORD': secrets.token_hex(24), 'REDIS_PASSWORD': redis,
'CELERY_BROKER_URL': f'redis://:{redis}@redis:6379/1',
'SANDBOX_API_KEY': sandbox, 'CODE_EXECUTION_API_KEY': sandbox,
'WEAVIATE_API_KEY': weaviate, 'WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS': weaviate,
'PLUGIN_DAEMON_KEY': t(), 'PLUGIN_DIFY_INNER_API_KEY': t(),
'DIFY_AGENT_API_TOKEN': t(), 'DIFY_AGENT_SERVER_SECRET_KEY': t(),
'DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN': t()}
s = open('.env').read()
for k, v in new.items():
s, n = re.subn(rf'^{k}=.*$', f'{k}={v}', s, flags=re.M)
assert n == 1, k
open('.env', 'w').write(s)
PY
INIT_PASSWORD is optional in Dify: when set, /install asks for it before creating the administrator, and it has no effect once setup is done. The docs cap it at 30 characters; the script's is 24. Read it with grep ^INIT_PASSWORD .env. Do not change SECRET_KEY later: the docs warn it logs everyone out, breaks file URLs and leaves plugins' OAuth credentials unrecoverable.
Then write the override and start the stack. docker compose ps should show every container Up or healthy, with init_permissions Exited as expected, and ss should list only 127.0.0.1:8080 and 127.0.0.1:5003.
cat > docker-compose.override.yaml <<'EOF'
services:
nginx:
ports: !override
- "127.0.0.1:8080:80"
plugin_daemon:
ports: !override
- "127.0.0.1:5003:5003"
EOF
docker compose up -d
docker compose ps
ss -ltnp | grep -E ':(80|443|5003|8080) '
First administrator, through a tunnel
- From your workstation, with your own login:
ssh -L 8080:127.0.0.1:8080 -N you@your-server. - Browse to
http://localhost:8080/install, give theINIT_PASSWORD, and create the administrator account. - Sign in at
http://localhost:8080. Self sign-up stays off:ALLOW_REGISTERdefaults to false, and colleagues join through workspace invitations. Without mail configured, the invitation page shows a link to forward by hand. - Only now publish the site through a proxy. A lost administrator password is reset with
docker exec -it docker-api-1 flask reset-password.
Putting it on a domain
Run a TLS proxy on the host in front of 127.0.0.1:8080 and open the web ports with ufw allow 80,443/tcp. What the docs ask of that proxy: pass the Upgrade and Connection headers on /socket.io/, where workflow collaboration runs over WebSockets, and set X-Forwarded-For. Our advice beyond the docs: match the bundled nginx's limits, 3600 s reads (NGINX_PROXY_READ_TIMEOUT, set for long SSE streams) and 100 MB request bodies.
Then name the public address in .env: CONSOLE_API_URL, CONSOLE_WEB_URL, SERVICE_API_URL, APP_API_URL, APP_WEB_URL and TRIGGER_URL become https://dify.example.com, NEXT_PUBLIC_SOCKET_URL becomes wss://dify.example.com, and the host in ENDPOINT_URL_TEMPLATE changes the same way. Restart as the docs do: docker compose down, then docker compose up -d.
From 1.18.0 Dify rate-limits sign-in and reset emails by the client address the bundled nginx reports, so behind your proxy every visitor shares one address. The docs' fix: read the address your proxy connects from in docker compose logs nginx rather than assuming 127.0.0.1, write set_real_ip_from with it, real_ip_header X-Forwarded-For and real_ip_recursive on into docker/nginx/conf.d/real-ip.conf, and reload with docker compose exec nginx nginx -s reload.
Local models: Ollama beside Dify
Install Ollama on the host as our Ollama server page shows. It listens on 127.0.0.1:11434 until OLLAMA_HOST says otherwise, and Dify's troubleshooting notes that containers cannot reach a host service on 127.0.0.1. Add ufw allow from 172.16.0.0/12 to any port 11434 proto tcp first. It matches on source address only: it admits sources in Docker's private bridge range (172.16.0.0/12), and public addresses stay blocked by the default deny. Then set OLLAMA_HOST=0.0.0.0:11434 in the Ollama service environment. The order matters: Ollama does not check who is calling.
In Dify, open Integrations > Model Provider, install the Ollama provider from the Marketplace, and add a model. The plugin's own examples use host.docker.internal or a LAN address, and Dify's Compose file maps no host.docker.internal, so on a bare Linux server our suggestion is base URL http://GATEWAY:11434, where GATEWAY is the host's address on Dify's network, printed as the gateway by docker network inspect docker_default. For a plain chat window on the same models, Open WebUI talks to that Ollama too. Pipelines that mostly shuttle data between SaaS tools, with a model call here and there, may sit better in n8n.
Backups and upgrades
For a Compose deployment the docs name the entire docker/volumes directory as the backup set. Keep .env with it, because a restore with a different SECRET_KEY logs every user out, invalidates file URLs and loses plugins' stored OAuth credentials. For a consistent copy, stop the stack: docker compose down, then umask 077; tar czf /root/dify-$(date +%F).tgz -C /opt/dify/docker volumes .env docker-compose.override.yaml, then docker compose up -d. The archive holds every secret; keep it mode 600 and copy it off the machine yourself.
The docs prescribe two things for an upgrade: follow the target release's notes on the GitHub releases page, and compare .env.example with your .env for new or changed variables. The rest is our own routine for a git checkout: back up, git fetch --tags and git checkout the new tag, do that comparison, then fetch the images with docker compose pull and recreate the containers with docker compose up -d. A release can add services, so run ss -ltnp again afterwards and extend the override if a new one publishes a port.
Licence, limits, and when to run it elsewhere
Dify ships under the Dify Open Source License, which the repository describes as based on Apache 2.0 with additional conditions. Commercial use is allowed, "including as a backend service for other applications or as an application development platform for enterprises." Two conditions bite. First: "Unless explicitly authorized by Dify in writing, you may not use the Dify source code to operate a multi-tenant environment", where one tenant is one workspace. Second, when you use its frontend, "you may not remove or modify the LOGO or copyright information in the Dify console or applications." Read the full text before building a product on it.
- Selling separate workspaces to many customers is a multi-tenant service: that needs a commercial licence from the producer.
- The docs place Kubernetes high availability, SSO and multi-workspace management under Dify Enterprise. This page covers the Community Edition on one host.
- One host is one failure domain. Compose here has no failover; if Dify must survive a hardware fault, keep a second machine and restore to it from your archives.
- If every model is a hosted API, a GPU build pays for a card that idles; a plain x86 line from the dedicated range is the better fit.
Questions
Does Dify need a GPU?
No. The platform's containers run on the CPU, and the documented minimum is 2 cores and 4 GiB of RAM. A GPU only matters when the models run on the same machine, for example under Ollama; then the model's size in memory decides the card.
How do I stop a stranger from claiming the admin account?
Set INIT_PASSWORD in .env, which /install then demands, and keep nginx published on 127.0.0.1 so the page is reachable only through your own SSH tunnel. Create the administrator that way, then put the proxy in front.
Can Dify use a model running on the same server?
Yes, through a model provider such as the Ollama plugin from the Dify Marketplace. Containers cannot reach a host service on 127.0.0.1, so Ollama has to listen on an address Dify's network reaches, with the firewall admitting only Docker's private range to port 11434.
Is self-hosted Dify free for commercial use?
The licence allows commercial use; its own examples are quoted in the licence section above. It requires a commercial licence from the producer to run a multi-tenant service, or to remove or change the logo and copyright in the frontend. Check the full licence text for your case.
How do I upgrade a Docker Compose install?
Read the target release's notes, back up docker/volumes and .env, check out the new tag, carry new variables from .env.example into .env, fetch images with docker compose pull, and recreate with docker compose up -d. Re-check the listening ports afterwards, since a release can add services.
What if containers misbehave after a reboot?
The services restart on their own. If they come up but cannot reach each other, the docs' remedy is docker compose down and docker compose up -d, then waiting for every container to report healthy before signing in.