跳到主要内容

Docker Compose

一篇面向新手的 Docker Compose 笔记:先讲清 compose.yaml 各部分的写法,再列出日常最常用的命令和生产环境的最佳实践,最后给出 9 个常见服务的完整部署案例,可以直接抄走改改用。

Compose 简介​

Docker Compose 是 Docker 官方的多容器编排工具:用一个 YAML 文件(默认叫 compose.yaml)把应用需要的所有服务、网络、数据卷写清楚,然后一条命令全部拉起来,不用再逐个敲 docker run。

现在说的 Compose 一般指 Compose v2。它是 Docker CLI 的插件,命令写成 docker compose(中间是空格,没有连字符),装 Docker Desktop 会自带;Linux 上装新版 Docker Engine 时带上 docker-compose-plugin 这个包即可。

老版本叫 Compose v1,是 Python 写的独立程序,命令是 docker-compose(有连字符),2023 年 7 月已经停止维护,不要再用了。两者的配置文件格式基本兼容,日常差别主要在命令写法和一些行为细节(比如 v2 生成的容器名用短横线分隔,v1 用下划线)。

# 查看 Compose 版本,能输出版本号就说明 v2 可用
docker compose version
Docker Compose version v2.29.1
备注

如果系统里还能敲出 docker-compose --version,那多半是历史遗留的 v1,建议卸载,避免和 v2 混用把自己搞混。

compose.yaml 结构​

一份 compose 文件由几个顶层字段组成:

  • services:必填,定义一个个容器,下面每个 key 就是一个服务
  • networks:可选,声明自定义网络
  • volumes:可选,声明命名数据卷
  • configs / secrets:可选,Swarm 场景用得多,单机一般不用
  • name:可选,项目名,决定生成的容器、网络、卷名字的前缀;不写就取所在目录名

最小示例,一个 Nginx 加一个 Redis:

name: demo

services:
web:
image: nginx:1.27
ports:
- "8080:80"

cache:
image: redis:7
volumes:
- cache-data:/data

volumes:
cache-data:

Compose 默认在当前目录依次找 compose.yaml、compose.yml、docker-compose.yml、docker-compose.yaml,文件名不在这几个里时用 -f 指定:

docker compose -f my.yaml up -d
提示

docker compose config 可以把变量替换、合并之后的最终配置打印出来。文件写复杂了先跑一遍这个命令自查,比直接 up 报错好排查。

文件顶部的 version 字段

老教程里的 compose 文件第一行常有 version: "3.8",这是 v1 时代的写法。v2 已经废弃这个字段,写了只会收到一条过时警告,直接删掉即可。

Services​

services 下每个条目对应一个容器,常用字段如下:

字段作用
image使用的镜像,建议带明确版本号
build用本地 Dockerfile 构建,和 image 二选一,也可以同时写(构建产物以 image 命名)
container_name固定容器名,不写会按「项目名-服务名-序号」生成
ports端口映射,格式 宿主机端口:容器端口
volumes挂载数据卷或宿主机目录
environment设置容器内环境变量
restart重启策略,生产常用 unless-stopped
depends_on控制启动顺序
command / entrypoint覆盖镜像默认的启动命令
networks指定接入哪些网络
services:
app:
image: nginx:1.27
container_name: app
restart: unless-stopped
ports:
- "8080:80"
environment:
TZ: Asia/Shanghai
volumes:
- ./html:/usr/share/nginx/html:ro
networks:
- web
ports 记得加引号

ports 的映射值一定写成字符串(加引号)。不加引号时 YAML 可能把 3306:3306 这类写法按六十进制时间格式解析,得到的端口完全不是你要的。

Networks​

不写 networks 时,Compose 会自动建一个默认网络(名字叫 项目名_default),所有服务都接进来,容器之间可以用服务名互相解析,比如在 web 容器里直接 ping cache。

显式声明网络的好处是隔离:只有接在同一个网络里的服务才能互相访问。

services:
web:
image: nginx:1.27
networks:
- frontend

api:
image: myapp:1.0
networks:
- frontend
- backend

db:
image: mysql:8.4
networks:
- backend # 只接 backend,web 访问不到数据库

networks:
frontend:
backend:

要接入已经存在的外部网络(比如另一个 compose 项目建好的网络),用 external: true:

networks:
shared:
external: true
信息

数据库这类服务尽量只接后端网络,不映射宿主机端口,这样外部完全摸不到它,只有同网络的应用能访问。

Volumes​

容器删了数据就没了,需要持久化的数据(数据库文件、上传的文件)必须挂出来。Compose 里常用两种挂载:

services:
db:
image: mysql:8.4
volumes:
- db-data:/var/lib/mysql # 命名卷,由 Docker 管理
- ./backup:/backup # 绑定挂载,宿主机目录
- ./my.cnf:/etc/mysql/my.cnf:ro # 挂载单个文件,只读

volumes:
db-data: # 顶层声明命名卷
  • 命名卷存在 Docker 管理的目录里(/var/lib/docker/volumes/),备份迁移方便,首选
  • 绑定挂载直接对应宿主机路径,适合配置文件、日志目录,改配置不用进容器
  • 挂载单个文件时宿主机路径写错、文件不存在,Docker 会把它当目录建出来,是常见坑

卷已经在外部建好、不想让 compose 管它的生命周期时,用 external: true:

volumes:
db-data:
external: true
down -v 会删卷

docker compose down -v 会把文件里声明的命名卷一起删掉,数据直接没了。生产环境执行 down 时想清楚要不要带 -v。

Environment​

environment 用来给容器设置环境变量,有 map 和列表两种写法,效果一样:

services:
db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: rootpass # map 写法
TZ: Asia/Shanghai
# 等价的列表写法:
# environment:
# - MYSQL_ROOT_PASSWORD=rootpass
# - TZ=Asia/Shanghai

变量较多时抽到 env_file:

services:
app:
image: myapp:1.0
env_file:
- ./app.env
提示

值是 yes、no、true、false 或纯数字这类词时建议加引号写成 "true",否则 YAML 会先把它解析成布尔值或数字,个别镜像拿到转换后的值会判断失误。

.env 文件​

.env 是给 compose 文件本身 做变量替换用的:Compose 启动时自动读取当前目录下的 .env,把 compose 文件里的 ${变量名} 替换成实际值。

.env 内容就是一行一个键值对:

MYSQL_ROOT_PASSWORD=s3cret
TZ=Asia/Shanghai

compose.yaml 里引用:

services:
db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
TZ: ${TZ:-UTC} # :- 表示 .env 里没设置时用默认值 UTC

注意区分两个概念,新手最容易搞混:

  • .env 加 ${...}:替换的是 compose 文件里的内容,变量要不要传进容器,取决于你把替换结果写在哪个字段
  • env_file 和 environment:直接设置容器内的环境变量,跟 compose 文件的变量替换无关

不想用默认文件名时用 --env-file 指定:

docker compose --env-file .env.prod up -d
不要把 .env 提交进 Git

.env 里一般都是密码和密钥,提交进仓库等于公开。在 .gitignore 里加上 .env,仓库里放一份不含真实值的 .env.example 给同事参考。

depends_on​

depends_on 控制启动顺序:被依赖的服务先启动,依赖方后启动。默认只等容器启动(service_started),但数据库容器跑起来了不代表数据库初始化完了,这时要用带条件的写法。

condition 的三种取值​

  • service_started:默认值,容器跑起来就算
  • service_healthy:等被依赖方的健康检查变成 healthy 才启动,前提是对方配好了 healthcheck
  • service_completed_successfully:等对方跑完并以退出码 0 退出,适合一次性初始化任务,比如建库脚本
services:
web:
image: myapp:1.0
depends_on:
db:
condition: service_healthy # 等数据库健康检查通过再启动
redis:
condition: service_started # 容器起来就行

db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: rootpass
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
interval: 10s
timeout: 5s
retries: 10

redis:
image: redis:7
$$ 是转义

compose 文件里想给容器传一个字面量的 $,要写成 $$。上面的 $$MYSQL_ROOT_PASSWORD 到了容器里才是 $MYSQL_ROOT_PASSWORD,由容器内的 shell 展开。写成单个 $ 会被 compose 自己先替换掉。

Healthcheck​

healthcheck 告诉 Docker 怎么判断容器里的应用是不是正常工作,配合 depends_on 的 service_healthy,才能实现「等数据库真的就绪再启动应用」。

services:
web:
image: nginx:1.27
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost/ || exit 1"]
interval: 30s # 每隔多久查一次
timeout: 5s # 单次检查的超时时间
retries: 3 # 连续失败几次判定 unhealthy
start_period: 10s # 启动后的宽限时间,期间失败不计入

test 的三种形式:

  • ["CMD", "curl", "-f", "http://localhost/"]:直接执行,不经过 shell
  • ["CMD-SHELL", "curl -f http://localhost/ || exit 1"]:交给 shell 执行,可以用管道和逻辑符
  • ["NONE"]:关掉镜像自带的 healthcheck

常见服务的检查命令:

服务检查命令
Web 服务curl -f http://localhost/ 或 wget --spider -q http://localhost/
MySQLmysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent
PostgreSQLpg_isready -U postgres
Redisredis-cli ping,有密码加 -a 密码 --no-auth-warning

启动后用 docker compose ps 看状态,健康的容器会带 (healthy) 标记:

NAME      STATUS                   PORTS
mysql Up 2 minutes (healthy) 0.0.0.0:3306->3306/tcp
注意

健康检查用的是容器内的命令。官方精简镜像里不一定有 curl 或 wget(比如 Nginx 官方镜像两个都没有),检查命令跑不起来会一直显示 unhealthy。选命令之前先 docker compose exec 服务名 which curl 确认一下。

Profiles​

一个 compose 文件里可以放平时不用的服务(调试工具、监控组件),给它们打上 profiles 标签,默认 up 不会启动,需要时按需拉起来。

services:
web:
image: nginx:1.27

db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: rootpass

adminer:
image: adminer:4
profiles: ["debug"] # 默认不启动
ports:
- "8081:8080"

规则很简单:不带 profile 的服务始终启动;带了 profile 的服务只有显式启用时才启动:

# 只起 web 和 db
docker compose up -d

# 把 debug 组的 adminer 一起起
docker compose --profile debug up -d

# 也可以用环境变量,省得每次敲
COMPOSE_PROFILES=debug docker compose up -d
提示

常见用法是把监控组件(Prometheus、Grafana)放进 monitoring 组,把 Adminer、phpMyAdmin 这类管理工具放进 debug 组。平时只跑核心服务,排查问题时再带上工具组。

常用命令​

以下命令都在 compose 文件所在目录执行;文件不在当前目录或文件名特殊时加 -f 文件名,想覆盖项目名加 -p 项目名。

命令作用常用参数
docker compose up创建并启动所有服务-d 后台运行;--build 先重新构建镜像;--force-recreate 强制重建容器
docker compose down停止并删除容器和网络-v 连命名卷一起删(慎用);--remove-orphans 清理已不在文件里的容器
docker compose ps查看各服务状态-a 把已停止的容器也列出来
docker compose logs查看日志-f 持续跟踪;--tail 100 只看最后 100 行;-t 带时间戳;后面跟服务名只看单个服务
docker compose exec在运行中的容器里执行命令例:docker compose exec db sh
docker compose pull拉取文件里声明的镜像拉完要 up -d 才会换新镜像重建
docker compose build构建带 build 字段的服务--no-cache 不用缓存全量构建
docker compose start / stop / restart启停已创建的服务后面跟服务名只操作单个
docker compose config校验并打印最终配置-q 只校验不输出,适合放在 CI 里
docker compose images列出各服务用到的镜像
docker compose top查看各容器内运行的进程
危险

docker compose down -v 删除命名卷后数据不可恢复,执行前确认数据已经备份或者真的不要了。

Compose 最佳实践​

下面几条都是踩过坑之后的经验,生产环境建议照做。

1. 固定镜像版本

latest 标签随时会变,哪天重新拉镜像被悄悄升级了大版本,挂了都不知道为什么。镜像一律写明确版本:

services:
db:
image: mysql:8.4 # 好
# image: mysql # 不好,等于 mysql:latest

2. 显式声明 networks 和 volumes

依赖 Compose 自动生成的默认网络和匿名卷,时间长了根本搞不清哪个卷是谁的。在顶层显式声明,名字见名知意,备份和清理都方便。

3. 配置重启策略

宿主机重启、容器异常退出之后,服务要能自己回来。生产用 unless-stopped(手动停掉的不会又被拉起),慎用 always(手动停了,宿主机一重启又起来了,容易忘):

services:
app:
image: myapp:1.0
restart: unless-stopped

4. 限制资源用量

不限内存时,一个服务内存泄漏能把整台宿主机拖垮。Compose v2 在单机模式下也认 deploy.resources 的 limits:

services:
app:
image: myapp:1.0
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M

5. 配置日志轮转

默认的 json-file 日志不做轮转,跑半年能把磁盘写满:

services:
app:
image: myapp:1.0
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"

6. 敏感信息只放 .env,不进 Git

密码、密钥写进 .env,compose 文件里用 ${变量名} 引用;.gitignore 忽略 .env,仓库里只放 .env.example。把密码写死在 compose 文件里再提交进仓库,是最常见的泄密方式。

7. 启动前用 config 自检

改了文件先 docker compose config -q 校验语法,确认变量替换结果符合预期,再 up。

8. 一个 compose 只管一组强相关服务

别把整台机器的所有服务塞进一个文件。按应用边界拆分,各管各的升级和回滚,出问题时影响面也小。

生产部署案例​

下面 9 个案例都是完整可运行的 compose.yaml,统一约定:

  • 密码等敏感值用 ${...} 从同目录的 .env 读取,动手前先建好 .env
  • 每个案例的 name 不同,互不干扰;宿主机端口冲突时改 ports 左边的值
  • 镜像版本以写作时的稳定版为准,正式部署前建议去 Docker Hub 或官方仓库核对一下最新版

MySQL​

name: mysql

services:
mysql:
image: mysql:8.4
container_name: mysql
restart: unless-stopped
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: appdb
TZ: Asia/Shanghai
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
networks:
- mysql-net

volumes:
mysql-data:

networks:
mysql-net:

说明:

  • mysql:8.4 是 8.x 的 LTS 版,新项目比 8.0 更合适
  • MYSQL_DATABASE 会在首次初始化时顺手建好一个业务库
  • 数据在命名卷 mysql-data 里,容器删了重建数据还在
  • 生产环境一般不对外映射 3306,让应用走内部网络访问即可

验证:

docker exec -it mysql mysql -uroot -p -e "SELECT VERSION();"
+-----------+
| VERSION() |
+-----------+
| 8.4.x |
+-----------+

Redis​

name: redis

services:
redis:
image: redis:7
container_name: redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"]
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli --no-auth-warning -a '${REDIS_PASSWORD}' ping | grep PONG"]
interval: 10s
timeout: 3s
retries: 5
networks:
- redis-net

volumes:
redis-data:

networks:
redis-net:

说明:

  • 官方镜像默认不开密码也不做持久化,command 里补上了 AOF 持久化和访问密码
  • --appendonly yes 把写操作追加到 AOF 文件,重启后能恢复数据
  • 密码通过 .env 注入,启动前确认 .env 里有 REDIS_PASSWORD=...

验证:

docker exec -it redis redis-cli -a '你的密码' --no-auth-warning ping
PONG

Nginx​

name: nginx

services:
nginx:
image: nginx:1.27
container_name: nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./conf.d:/etc/nginx/conf.d:ro
- ./html:/usr/share/nginx/html:ro
- nginx-logs:/var/log/nginx
networks:
- web

volumes:
nginx-logs:

networks:
web:

配置和站点目录用绑定挂载放在宿主机上,改完配置执行 docker compose exec nginx nginx -s reload 平滑生效,不用重建容器。一个最小的 conf.d/default.conf:

server {
listen 80;
server_name _;

root /usr/share/nginx/html;
index index.html;
}
为什么没有 healthcheck

Nginx 官方镜像很精简,没有 curl 也没有 wget,容器内做不了 HTTP 健康检查。要么不查(Nginx 进程本身很稳定),要么在 Dockerfile 里装个 curl 自建镜像。

验证(在宿主机上执行):

curl -I http://localhost
HTTP/1.1 200 OK
Server: nginx/1.27.x

Open WebUI​

Ollama 或 OpenAI 兼容接口的 Web 聊天界面,自己搭 AI 助手常用。

name: open-webui

services:
open-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui
restart: unless-stopped
ports:
- "3000:8080"
environment:
OLLAMA_BASE_URL: http://host.docker.internal:11434 # 不用本地 Ollama 可以删掉
extra_hosts:
- "host.docker.internal:host-gateway" # Linux 下让容器能访问宿主机
volumes:
- open-webui-data:/app/backend/data
networks:
- ai

volumes:
open-webui-data:

networks:
ai:

说明:

  • 对话记录、账号、上传的文件都存在 /app/backend/data,必须挂卷
  • main 标签是滚动更新的,想固定版本就去 GitHub Releases 找具体版本号替换
  • 首次启动要初始化,等十几秒再访问;浏览器打开 http://localhost:3000,第一个注册的账号自动成为管理员

验证:

curl http://localhost:3000/health
{"status":true}

Prometheus​

name: prometheus

services:
prometheus:
image: prom/prometheus:v3.2.1
container_name: prometheus
restart: unless-stopped
ports:
- "9090:9090"
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.retention.time=15d"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:9090/-/healthy"]
interval: 15s
timeout: 5s
retries: 5
networks:
- monitor

volumes:
prometheus-data:

networks:
monitor:

配置文件 prometheus.yml 和 compose 放同一目录,最小配置:

global:
scrape_interval: 15s

scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]

说明:

  • 要监控别的服务时,把对应的 exporter 加进 scrape_configs
  • 时序数据在 /prometheus,已挂命名卷;--storage.tsdb.retention.time 控制数据保留时长
  • 镜像基于 busybox,自带 wget,可以直接在容器内做健康检查

验证:

curl http://localhost:9090/-/healthy
Prometheus Server is Healthy.

Grafana​

name: grafana

services:
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- grafana-data:/var/lib/grafana
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:3000/api/health || exit 1"]
interval: 15s
timeout: 5s
retries: 5
start_period: 20s
networks:
- monitor

volumes:
grafana-data:

networks:
monitor:

说明:

  • 仪表盘、数据源配置都在 /var/lib/grafana,挂卷后重建容器不丢配置
  • 管理员密码用 .env 注入,不要沿用默认的 admin/admin
  • 对接上面案例里的 Prometheus 时,数据源地址填 http://prometheus:9090,前提是两个服务在同一个网络里(把 networks 并成一个,或者用 external 网络)

验证:

curl http://localhost:3000/api/health
{"commit":"...","database":"ok","version":"..."}

浏览器打开 http://localhost:3000 登录。

版本固定

示例图省事用了 latest,生产建议换成明确版本号(比如 grafana/grafana:11.x 这类),升级前先备份数据卷。

VictoriaLogs​

VictoriaMetrics 家的日志数据库,资源占用比 ELK 低很多,适合中小规模的日志收集。

name: victorialogs

services:
victoria-logs:
image: victoriametrics/victoria-logs:v1.18.0-victorialogs
container_name: victoria-logs
restart: unless-stopped
ports:
- "9428:9428"
command:
- "-retentionPeriod=3"
- "-storageDataPath=/victoria-logs-data"
volumes:
- vlogs-data:/victoria-logs-data
networks:
- logs

volumes:
vlogs-data:

networks:
logs:

说明:

  • -retentionPeriod=3 表示日志保留 3 个月,单位是月
  • 镜像从 scratch 构建,里面没有 shell、没有 curl,做不了容器内健康检查,在宿主机上验证即可
  • 写入和查询都走 9428 端口的 HTTP API,兼容 Logstash、Filebeat 等主流采集器的协议

验证:

# 服务健康检查
curl http://localhost:9428/health
OK
# 写入一条日志
curl http://localhost:9428/insert/jsonline?_stream_fields=app \
-H 'Content-Type: application/stream+json' \
-d '{"log":{"level":"info"},"app":"demo","message":"hello victorialogs"}'

# 查询刚写入的日志
curl 'http://localhost:9428/select/logsql/query' -d 'query=app:demo'
信息

镜像 tag 带 -victorialogs 后缀是历史命名,新版本的 tag 可能直接就是版本号。部署前去 Docker Hub 的 victoriametrics/victoria-logs 页面确认一下最新 tag。

n8n​

工作流自动化工具,相当于自建版 Zapier。

name: n8n

services:
n8n:
image: docker.n8n.io/n8nio/n8n
container_name: n8n
restart: unless-stopped
ports:
- "5678:5678"
environment:
N8N_HOST: localhost
N8N_PROTOCOL: http
N8N_SECURE_COOKIE: "false" # 纯 HTTP 访问必须关掉,否则登录不了
GENERIC_TIMEZONE: Asia/Shanghai
TZ: Asia/Shanghai
volumes:
- n8n-data:/home/node/.n8n
healthcheck:
test: ["CMD-SHELL", "wget --spider -q http://127.0.0.1:5678/healthz || exit 1"]
interval: 15s
timeout: 5s
retries: 5
start_period: 20s
networks:
- automation

volumes:
n8n-data:

networks:
automation:

说明:

  • 工作流、凭证、执行记录默认存在内嵌 SQLite,数据都在 /home/node/.n8n
  • 容器内进程以 node 用户(uid 1000)运行,用命名卷没有权限问题;换成绑定挂载要注意目录属主
  • 示例没固定镜像版本,生产建议写成 docker.n8n.io/n8nio/n8n:1.x.y 这样的具体版本
  • 规模大了可以把存储换成 PostgreSQL:加 DB_TYPE=postgresdb、DB_POSTGRESDB_HOST 等环境变量
  • 浏览器打开 http://localhost:5678,首次访问会引导创建管理员账号

验证:

curl -I http://localhost:5678/healthz
HTTP/1.1 200 OK

Dify​

Dify 官方推荐的部署方式不是手写一份 compose,而是直接用官方仓库里维护好的整套配置:

git clone https://github.com/langgenius/dify.git
cd dify/docker
cp .env.example .env
docker compose up -d

官方这套包含 api、worker、web、PostgreSQL、Redis、向量库(默认 Weaviate)、sandbox、ssrf_proxy、nginx 等十几个服务,.env 里有上百个配置项,启动前至少把 SECRET_KEY 和 INIT_PASSWORD 改掉。好处是功能全、升级有官方维护,代价是资源占用不小,建议 4 核 8G 起步。

注意

官方 docker-compose.yaml 和 Dify 的版本是绑定的。升级 Dify 要拉取仓库新代码,用新目录里的配置重启,不要拿旧的 compose 文件只改镜像版本号。

如果只想快速体验一下,可以用下面这个精简版(只有 api、worker、web、PostgreSQL、Redis,用 pgvector 当向量库):

name: dify-mini

x-api-env: &api-env
SECRET_KEY: ${DIFY_SECRET_KEY}
INIT_PASSWORD: ${DIFY_INIT_PASSWORD}
CONSOLE_API_URL: http://localhost:5001
CONSOLE_WEB_URL: http://localhost:3000
SERVICE_API_URL: http://localhost:5001
APP_API_URL: http://localhost:5001
APP_WEB_URL: http://localhost:3000
DB_USERNAME: postgres
DB_PASSWORD: ${DIFY_DB_PASSWORD}
DB_HOST: db
DB_PORT: 5432
DB_DATABASE: dify
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${DIFY_REDIS_PASSWORD}
REDIS_USE_SSL: "false"
CELERY_BROKER_URL: redis://:${DIFY_REDIS_PASSWORD}@redis:6379/1
STORAGE_TYPE: local
STORAGE_LOCAL_PATH: /app/api/storage
VECTOR_STORE: pgvector
PGVECTOR_HOST: db
PGVECTOR_PORT: 5432
PGVECTOR_USER: postgres
PGVECTOR_PASSWORD: ${DIFY_DB_PASSWORD}
PGVECTOR_DATABASE: dify

services:
api:
image: langgenius/dify-api:0.15.3
restart: unless-stopped
ports:
- "5001:5001"
environment:
<<: *api-env
MODE: api
volumes:
- dify-storage:/app/api/storage
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- dify-net

worker:
image: langgenius/dify-api:0.15.3
restart: unless-stopped
environment:
<<: *api-env
MODE: worker
volumes:
- dify-storage:/app/api/storage
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- dify-net

web:
image: langgenius/dify-web:0.15.3
restart: unless-stopped
ports:
- "3000:3000"
environment:
CONSOLE_API_URL: http://localhost:5001
APP_API_URL: http://localhost:5001
depends_on:
- api
networks:
- dify-net

db:
image: pgvector/pgvector:pg16
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DIFY_DB_PASSWORD}
POSTGRES_DB: dify
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- dify-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 10
networks:
- dify-net

redis:
image: redis:6-alpine
restart: unless-stopped
command: ["redis-server", "--requirepass", "${DIFY_REDIS_PASSWORD}"]
volumes:
- dify-redis:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli --no-auth-warning -a '${DIFY_REDIS_PASSWORD}' ping | grep PONG"]
interval: 10s
timeout: 3s
retries: 5
networks:
- dify-net

volumes:
dify-storage:
dify-db:
dify-redis:

networks:
dify-net:

说明:

  • 顶部的 x-api-env 加 <<: *api-env 是 YAML 锚点写法,让 api 和 worker 共用一套环境变量,compose 支持这种语法,省去大段重复
  • 精简版砍掉了 sandbox(代码执行节点不可用)和 ssrf_proxy,只适合体验;正式使用请回到官方部署方式
  • 官方部署前面还有一层 nginx 做统一入口,精简版让浏览器直连 5001 端口的 api,只建议本地用
  • 精简版基于 0.15.3 的架构,1.x 之后官方架构加了 plugin_daemon 等组件,变化很大,镜像版本以官方 dify/docker 目录里 .env.example 的推荐为准
  • .env 里需要准备 DIFY_SECRET_KEY、DIFY_INIT_PASSWORD、DIFY_DB_PASSWORD、DIFY_REDIS_PASSWORD 四个变量

验证:

# 看 api 日志,没有报错就说明起来了
docker compose logs api | tail -5

# api 健康检查
curl http://localhost:5001/health
{"status": "ok", "version": "0.15.3"}

浏览器打开 http://localhost:3000/install,用 INIT_PASSWORD 完成管理员初始化。