Dify NAS Docker 部署踩坑指南
在低功耗 NAS 上用 Docker Compose 部署 Dify,前前后后折腾了两天,踩遍了几乎所有能踩的坑。如果你也在 NAS、软路由或者低配服务器上部署 Dify,希望这篇能帮你少走弯路。
参考文档
环境说明
本文基于以下环境,但大部分问题在任何 Docker Compose 部署场景下都可能遇到:
- 绿联 DXP-2800 NAS,Intel N100 处理器(4 核),8GB 内存
- Debian 12,Docker Compose 部署
- 数据目录通过 SMB/NFS 网络挂载(权限问题的根源)
- Dify 版本:1.11.1
服务架构
Dify 由 10 个容器组成,缺一不可:
| 服务 | 用途 |
|---|---|
| nginx | 反向代理,统一入口 |
| web | Next.js 前端,提供控制台和应用界面 |
| api | Flask 后端,处理所有业务请求 |
| worker | Celery 异步任务,如知识库处理 |
| worker_beat | Celery 定时调度 |
| plugin_daemon | 插件管理守护进程 |
| sandbox | 代码执行沙箱(隔离环境) |
| ssrf_proxy | Squid 代理,防止 SSRF 攻击 |
| postgres | PostgreSQL 16 + pgvector 向量扩展 |
| redis | 缓存 + Celery 消息队列 |
项目文件结构
部署前需要在 NAS 上准备好以下目录和文件(路径根据实际情况调整,本文以 /opt/dify/ 为例):
/opt/dify/
├── docker-compose.yaml # 核心编排文件
├── nginx/
│ └── nginx.conf # 反向代理路由配置
└── ssrf_proxy/
└── squid.conf # SSRF 防护规则
docker-compose.yaml
⚠️ 使用前务必替换其中的密码和密钥。
{NAS_IP}替换为你的 NAS IP,所有密码类字段(POSTGRES_PASSWORD、REDIS_PASSWORD、SECRET_KEY等)改成你自己的。
services:
# ============================================================
# PostgreSQL 16 + pgvector
# ============================================================
postgres:
image: pgvector/pgvector:pg16
container_name: dify-postgres
restart: always
volumes:
- ./pgdata:/var/lib/postgresql/data
networks:
- dify-network
environment:
- TZ=Asia/Shanghai
- POSTGRES_USER=dify
- POSTGRES_PASSWORD=你的数据库密码
- POSTGRES_DB=dify
- POSTGRES_SHARED_BUFFERS=256MB
- POSTGRES_EFFECTIVE_CACHE_SIZE=768MB
- POSTGRES_MAINTENANCE_WORK_MEM=128MB
- POSTGRES_WORK_MEM=32MB
- POSTGRES_MAX_CONNECTIONS=50
- POSTGRES_RANDOM_PAGE_COST=3.0
- POSTGRES_EFFECTIVE_IO_CONCURRENCY=2
- POSTGRES_CHECKPOINT_COMPLETION_TARGET=0.9
- POSTGRES_CHECKPOINT_TIMEOUT=15min
- POSTGRES_WAL_BUFFERS=16MB
- POSTGRES_DEFAULT_STATISTICS_TARGET=500
- POSTGRES_JIT=off
- POSTGRES_MAX_WORKER_PROCESSES=4
- POSTGRES_MAX_PARALLEL_WORKERS_PER_GATHER=2
- POSTGRES_MAX_PARALLEL_WORKERS=4
mem_limit: 384m
cpus: 1
shm_size: 256m
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dify -d dify || exit 1"]
interval: 5s
timeout: 3s
retries: 5
start_period: 5s
# ============================================================
# Redis 7 - 缓存与消息队列
# ============================================================
redis:
image: redis:7-alpine
container_name: dify-redis
restart: always
command:
- "redis-server"
- "--requirepass"
- "你的Redis密码"
- "--maxmemory"
- "256mb"
- "--maxmemory-policy"
- "allkeys-lru"
- "--save"
- "" # 关键:禁用 RDB 持久化
- "--stop-writes-on-bgsave-error"
- "no" # 关键:磁盘写入失败也不报错
volumes:
- ./redis_data:/data
networks:
- dify-network
mem_limit: 256m
cpus: 0.5
healthcheck:
test: ["CMD", "redis-cli", "-a", "你的Redis密码", "--no-auth-warning", "ping"]
interval: 10s
timeout: 5s
retries: 3
# ============================================================
# Sandbox - 代码执行沙箱
# ============================================================
sandbox:
image: langgenius/dify-sandbox:0.2.10
container_name: dify-sandbox
restart: always
networks:
- dify-network
environment:
- API_KEY=你的内部API密钥
- GIN_MODE=release
- WORKER_TIMEOUT=15
- ENABLE_NETWORK=true
- HTTP_PROXY=http://ssrf_proxy:3128
- HTTPS_PROXY=http://ssrf_proxy:3128
- SANDBOX_PORT=8194
mem_limit: 256m
cpus: 0.5
security_opt:
- "no-new-privileges:true"
# ============================================================
# SSRF Proxy
# ============================================================
ssrf_proxy:
image: ubuntu/squid:latest
container_name: dify-ssrf-proxy
restart: always
volumes:
- ./ssrf_proxy/squid.conf:/etc/squid/squid.conf:ro
networks:
- dify-network
mem_limit: 128m
cpus: 0.5
# ============================================================
# Dify API 服务
# ============================================================
api:
image: langgenius/dify-api:1.11.1
container_name: dify-api
restart: always
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
sandbox:
condition: service_started
volumes:
- ./storage:/app/api/storage
- /etc/localtime:/etc/localtime:ro
networks:
- dify-network
environment:
- TZ=Asia/Shanghai
- MODE=api
- DB_TYPE=postgresql
- DB_USERNAME=dify
- DB_PASSWORD=你的数据库密码
- DB_HOST=postgres
- DB_PORT=5432
- DB_DATABASE=dify
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=你的Redis密码
- CELERY_BROKER_URL=redis://:你的Redis密码@redis:6379/1
- SECRET_KEY=你的Flask密钥(至少32位随机字符串)
- CODE_EXECUTION_ENDPOINT=http://sandbox:8194
- CODE_EXECUTION_API_KEY=你的内部API密钥
- SSRF_PROXY_HTTP_URL=http://ssrf_proxy:3128
- SSRF_PROXY_HTTPS_URL=http://ssrf_proxy:3128
- STORAGE_TYPE=local
- STORAGE_LOCAL_PATH=/app/api/storage
- INIT_PASSWORD=你的Dify初始管理员密码
- LOG_LEVEL=INFO
- CONSOLE_WEB_URL=http://{NAS_IP}:8080
- APP_WEB_URL=http://{NAS_IP}:8080
- PLUGIN_DAEMON_KEY=你的内部API密钥 # 必须和 plugin_daemon 的 SERVER_KEY 一致
- PLUGIN_DAEMON_URL=http://plugin_daemon:5002
- INNER_API_KEY_FOR_PLUGIN=你的内部API密钥 # 必须和上面一致
- PLUGIN_REMOTE_INSTALL_HOST=plugin_daemon
- PLUGIN_REMOTE_INSTALL_PORT=5003
- PLUGIN_MAX_PACKAGE_SIZE=52428800
mem_limit: 1024m
cpus: 1.5
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5001/health"]
interval: 30s
timeout: 30s # 低配机器必须放宽,10s 不够
retries: 5
start_period: 120s # 给足 2 分钟启动时间
# ============================================================
# Dify Worker - 后台异步任务
# ============================================================
worker:
image: langgenius/dify-api:1.11.1
container_name: dify-worker
restart: always
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
sandbox:
condition: service_started
volumes:
- ./storage:/app/api/storage
- /etc/localtime:/etc/localtime:ro
networks:
- dify-network
environment:
- TZ=Asia/Shanghai
- MODE=worker
- DB_TYPE=postgresql
- DB_USERNAME=dify
- DB_PASSWORD=你的数据库密码
- DB_HOST=postgres
- DB_PORT=5432
- DB_DATABASE=dify
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=你的Redis密码
- CELERY_BROKER_URL=redis://:你的Redis密码@redis:6379/1
- SECRET_KEY=你的Flask密钥
- CODE_EXECUTION_ENDPOINT=http://sandbox:8194
- CODE_EXECUTION_API_KEY=你的内部API密钥
- SSRF_PROXY_HTTP_URL=http://ssrf_proxy:3128
- SSRF_PROXY_HTTPS_URL=http://ssrf_proxy:3128
- STORAGE_TYPE=local
- STORAGE_LOCAL_PATH=/app/api/storage
- LOG_LEVEL=INFO
- PLUGIN_DAEMON_KEY=你的内部API密钥
- PLUGIN_DAEMON_URL=http://plugin_daemon:5002
- INNER_API_KEY_FOR_PLUGIN=你的内部API密钥
- PLUGIN_MAX_PACKAGE_SIZE=52428800
mem_limit: 768m
cpus: 1.5
# ============================================================
# Dify Worker Beat - 定时任务调度
# ============================================================
worker_beat:
image: langgenius/dify-api:1.11.1
container_name: dify-worker-beat
restart: always
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
networks:
- dify-network
environment:
- TZ=Asia/Shanghai
- MODE=beat
- DB_TYPE=postgresql
- DB_USERNAME=dify
- DB_PASSWORD=你的数据库密码
- DB_HOST=postgres
- DB_PORT=5432
- DB_DATABASE=dify
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=你的Redis密码
- CELERY_BROKER_URL=redis://:你的Redis密码@redis:6379/1
- SECRET_KEY=你的Flask密钥
- STORAGE_TYPE=local
- STORAGE_LOCAL_PATH=/app/api/storage
- LOG_LEVEL=INFO
mem_limit: 256m
cpus: 0.5
# ============================================================
# Plugin Daemon - 插件管理
# ============================================================
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.5.1-local
container_name: dify-plugin-daemon
restart: always
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
volumes:
- ./plugin_daemon:/app/storage
networks:
- dify-network
environment:
- TZ=Asia/Shanghai
- DB_USERNAME=dify
- DB_PASSWORD=你的数据库密码
- DB_HOST=postgres
- DB_PORT=5432
- DB_DATABASE=dify
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=你的Redis密码
- SERVER_PORT=5002
- SERVER_KEY=你的内部API密钥 # 必须和 api 的 PLUGIN_DAEMON_KEY 一致
- DIFY_INNER_API_URL=http://api:5001
- DIFY_INNER_API_KEY=你的内部API密钥
- PLUGIN_REMOTE_INSTALLING_HOST=0.0.0.0
- PLUGIN_REMOTE_INSTALLING_PORT=5003
- PLUGIN_WORKING_PATH=/app/storage/cwd
- PLUGIN_STORAGE_TYPE=local
- PLUGIN_STORAGE_LOCAL_ROOT=/app/storage
- PLUGIN_INSTALLED_PATH=plugin
- PLUGIN_PACKAGE_CACHE_PATH=plugin_packages
- PLUGIN_MEDIA_CACHE_PATH=assets
- MAX_PLUGIN_PACKAGE_SIZE=52428800
- PLUGIN_MAX_EXECUTION_TIMEOUT=600
- PYTHON_ENV_INIT_TIMEOUT=120
- FORCE_VERIFYING_SIGNATURE=true
- PPROF_ENABLED=false
mem_limit: 1024m # 512M 不够,至少 1G
cpus: 1
ports:
- "5003:5003"
# ============================================================
# Dify Web 前端
# ============================================================
web:
image: langgenius/dify-web:1.11.1
container_name: dify-web
restart: always
depends_on:
- api
networks:
- dify-network
environment:
- CONSOLE_API_URL= # 留空!让前端用相对路径
- APP_API_URL= # 留空!
- NEXT_PUBLIC_DEPLOY_ENV=PRODUCTION
mem_limit: 512m
cpus: 1
# ============================================================
# Nginx 反向代理
# ============================================================
nginx:
image: nginx:alpine
container_name: dify-nginx
restart: always
depends_on:
- api
- web
ports:
- "8080:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./storage:/app/api/storage:ro
networks:
- dify-network
mem_limit: 128m
cpus: 0.5
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 30s
timeout: 5s
retries: 3
networks:
dify-network:
driver: bridge
nginx/nginx.conf
关键:location 的书写顺序决定了路由优先级。
/console/api/必须放在/console/前面,否则控制台 API 请求会被误路由到前端,返回 HTML 而非 JSON。
worker_processes auto;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
client_max_body_size 100m;
gzip on;
server {
listen 80;
# 公共 API → Flask
location /api/ {
proxy_pass http://api:5001/api/;
proxy_read_timeout 300s;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 插件 API → Plugin Daemon
location /plugin/ {
proxy_pass http://plugin_daemon:5002/plugin/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 控制台 API → Flask(必须在 /console/ 前面!)
location /console/api/ {
proxy_pass http://api:5001/console/api/;
proxy_read_timeout 300s;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 文件访问
location /files/ {
alias /app/api/storage/;
}
# 前端页面 → Next.js
location /console/ { proxy_pass http://web:3000/console/; proxy_set_header Host $host; }
location /app/ { proxy_pass http://web:3000/app/; proxy_set_header Host $host; }
location /explore/ { proxy_pass http://web:3000/explore/; proxy_set_header Host $host; }
location /_next/ { proxy_pass http://web:3000/_next/; proxy_set_header Host $host; }
location / { proxy_pass http://web:3000/; proxy_set_header Host $host; }
}
}
ssrf_proxy/squid.conf
http_port 3128
acl localnet dst 10.0.0.0/8
acl localnet dst 172.16.0.0/12
acl localnet dst 192.168.0.0/16
acl localnet dst 127.0.0.0/8
acl localnet dst 169.254.0.0/16
acl safe_ports port 80
acl safe_ports port 443
acl safe_ports port 8080
acl safe_ports port 8443
http_access deny !safe_ports
http_access deny localnet
http_access allow all
cache deny all
refresh_pattern . 0 20% 4320
coredump_dir /var/spool/squid
踩坑全记录
坑 1:网络存储的权限问题(踩了无数次)
现象:api 和 worker 反复重启,日志显示 PermissionError: [Errno 13] Permission denied。
原因:如果你把 docker-compose 的 volume 目录放在 SMB/NFS 网络挂载路径上(比如 NAS 通过 Samba 挂到电脑上编辑文件),容器内部运行的用户(通常是 uid=1000 或 1001)和挂载目录的属主不一致,加上网络文件系统对 chown 的支持有限,容器无法在其中创建文件和目录。
解决方案:
# SSH 到 NAS 上直接执行,不要通过 SMB 客户端
chmod -R 777 ./storage
chmod -R 777 ./redis_data
chmod -R 777 ./plugin_daemon
通用教训:volume 目录尽量放在 NAS 本地文件系统上,避免通过网络挂载后再给 Docker 用。如果必须用网络挂载,部署前先 chmod 777。
坑 2:Redis 无法持久化,整个 API 瘫痪
现象:登录后所有操作 500,日志显示 MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk。
原因:Redis 默认要写 RDB 快照到 /data 目录。如果该目录因权限问题写不进去,Redis 会拒绝所有写操作——包括 Session、缓存等正常业务操作。
解决方案——在 Redis 命令中加两项:
command:
- "--save"
- "" # 禁用 RDB
- "--stop-writes-on-bgsave-error"
- "no" # 写入失败也不拒绝请求
坑 3:低配 CPU 导致健康检查超时
现象:容器启动了但显示 unhealthy,随后整个部署失败。
原因:Dify API 启动时要初始化 gevent 协程池、gRPC 补丁、psycopg2 补丁、NumExpr 线程池等,N100 这类低功耗 CPU 上首次 /health 响应可能超过 20 秒,而默认的 10 秒超时根本不够。
解决方案——把健康检查参数调松:
healthcheck:
timeout: 30s # 至少 30s
retries: 5 # 多给几次机会
start_period: 120s # 启动后等 2 分钟再查
坑 4:nginx location 顺序错误
现象:页面能打开但只显示 Logo,控制台报 /api/console/api/system-features 404,返回 HTML 而非 JSON。
原因:Dify 的架构中,API 请求和页面请求走的是同一个 80 端口:
/api/*、/console/api/*、/plugin/*→ 后端/console/*、/app/*→ 前端
nginx 的 location 规则是最长前缀优先。如果把 /console/ 写在 /console/api/ 前面,/console/api/* 就会被前端路由吞掉,返回 Next.js 的 HTML 页面导致 JSON 解析报错。
正确顺序:/api/ → /plugin/ → /console/api/ → /console/ → /app/ → /
坑 5:CONSOLE_API_URL 和 APP_API_URL 配置错误
现象:URL 路径出现重复,如 /console/api/console/api/...。
原因:这两个环境变量是用来覆盖 API 基地址的。如果填了具体路径(如 http://ip:8080/api),前端会把全部端点路径拼在后面导致重复。
解决方案:留空。 Next.js 在服务端渲染时会自动使用相对路径,nginx 会正确路由。
坑 6:Plugin Daemon 与 API 的密钥不一致
现象:右上角弹出 Failed to request plugin daemon,API 日志显示 401。
原因:API 调用 plugin_daemon 时,HTTP 头里带的是 PLUGIN_DAEMON_KEY 的值;而 plugin_daemon 校验的是 SERVER_KEY。两者必须一致。
另外 INNER_API_KEY_FOR_PLUGIN 是插件回调 API 用的,和 PLUGIN_DAEMON_KEY 是两个不同的变量。
解决方案:三把钥匙设成同一个值 —— 在 api/worker 中设置 PLUGIN_DAEMON_KEY,在 plugin_daemon 中设置 SERVER_KEY,值完全一致。
坑 7:Plugin Daemon 必填环境变量缺失
现象:plugin_daemon 启动后立即 panic 退出,反复重启。
原因:plugin_daemon 是 Go 写的,启动时会做严格的配置校验,缺少任何一个必填项直接 panic。除了数据库 /Redis 连接信息,还必须有:
SERVER_KEY:自身 API 的鉴权密钥DIFY_INNER_API_URL:回调 Dify API 的地址PLUGIN_WORKING_PATH:插件运行时的工作目录PLUGIN_STORAGE_TYPE+PLUGIN_STORAGE_LOCAL_ROOT:存储配置FORCE_VERIFYING_SIGNATURE:插件签名校验
坑 8:Plugin Daemon OOM
现象:plugin_daemon 运行一段时间后退出,退出码 137。
原因:137 = 被 Linux OOM Killer 杀掉。安装插件时会拉取、编译 Python 依赖,安装多个插件时内存占用轻松突破 512MB。
解决方案:mem_limit: 1024m,至少 1GB。
坑 9:忘记跑数据库迁移
现象:所有容器 healthy,但页面全部 500,日志显示 relation "dify_setups" does not exist。
原因:PostgreSQL 容器会自动创建数据库,但不会自动建表。Dify 的表结构由 Flask-Migrate 管理。
解决方案(容器启动后执行一次):
docker exec dify-api flask db upgrade
坑 10:国内网络拉不动镜像
现象:docker pull 几小时没动静或中途 EOF。
原因:Dify API 镜像约 2.5GB,从 Docker Hub 直拉几乎不可能。
变通方案:
- 可以试试
docker.1ms.run这类国内镜像站,但不一定稳定 - 开代理拉,然后
docker save导出再导入到 NAS - 如果已有能用的旧版本,先用着别追新
部署检查清单
- 替换 docker-compose.yaml 中所有
{NAS_IP}和密码占位符 - volume 目录已创建并
chmod 777 - nginx.conf 和 squid.conf 已放到对应子目录
-
docker-compose up -d后等 3 分钟(低配机器需要) -
docker ps10 个容器全部 Up,无 Restarting -
docker logs dify-api无 Permission denied -
docker exec dify-api flask db upgrade执行成功 -
curl http://{NAS_IP}:8080/console/api/system-features返回 JSON - 浏览器访问
http://{NAS_IP}:8080/install完成初始化
部署时间:2026 年 5 月,累计踩坑 10+,重启容器无数次。
评论交流
欢迎留下你的想法