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反向代理,统一入口
webNext.js 前端,提供控制台和应用界面
apiFlask 后端,处理所有业务请求
workerCelery 异步任务,如知识库处理
worker_beatCelery 定时调度
plugin_daemon插件管理守护进程
sandbox代码执行沙箱(隔离环境)
ssrf_proxySquid 代理,防止 SSRF 攻击
postgresPostgreSQL 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_PASSWORDREDIS_PASSWORDSECRET_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 ps 10 个容器全部 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+,重启容器无数次。