CookCLI Docker Compose 部署指南
CookCLI 0.39.0 官方镜像 Docker Compose 部署 cook server 食谱 Web 服务:镜像与安装路径选型、食谱库初始化、users.toml 签名登录、数据持久化、环境变量与服务器参数、等价 docker run 与运维命令,Ubuntu 24.04 实测。
CookCLI Docker Compose 部署指南
目标:在 Ubuntu 24.04 服务器上以 Docker Compose 部署 CookCLI 0.39.0 官方镜像,把
cook server作为常驻食谱 Web 服务跑起来——覆盖镜像与安装路径选型、食谱库初始化、登录用户配置、数据持久化、环境变量与服务器参数、等价 docker run 及日常运维命令。全流程已在 Ubuntu 24.04 + Docker 29.9.2 + Compose v5.6.1 实测通过。
一、环境信息
| 项 | 值 |
|---|---|
| 操作系统 | Ubuntu 24.04 LTS(x86_64) |
| Docker 引擎 | Docker version 29.9.2, build a82e528 |
| Compose | Docker Compose version v5.6.1 |
| 镜像 | ghcr.io/cooklang/cookcli:0.39.0 |
| 镜像 Digest | sha256:fc4943d8c127f76eb65fddf85a17fed39cfa317d40fbce2f759cd175bf6f62a0 |
| 服务端口 | 宿主机 8138 → 服务内 9080 |
| 项目目录 | /opt/cookcli/(recipes/ 食谱库 + config/ 配置目录) |
| 软件版本 | CookCLI v0.39.0(2026-10-24 发布,版本溯源见 《CookCLI 介绍与选型指南》) |
实测回显(版本取证):
docker --version
# Docker version 29.9.2, build a82e528
docker compose version
# Docker Compose version v5.6.1
💡 技术拓展:CookCLI 的部署形态不止容器一种,常见路径对比——
形态 方式 特点 适用 预编译二进制 Releases 下载 cook-<版本>-linux-x86_64-musl.tar.gz单文件约 23 MB,musl 静态不依赖发行版 C 库, cook update可自更新裸机/NAS,只要 CLI Cargo cargo install cookcli --locked可裁剪 features(server/import/lsp/sync/self-update),依赖图 412→225 crate,产物可缩至约 9 MB 已有 Rust 工具链、只要核心功能 系统包 apt 的 .deb/ dnf 的.rpm进发行版包管理,随 apt upgrade升级Debian 12+/Ubuntu 22.04+/Fedora 官方容器镜像 ghcr.io/cooklang/cookcli固定入口 cook server /recipes --host,升级换 tag 即可需要常驻 Web 服务、与宿主工具链隔离 本文因目标是「服务器常驻 Web 服务 + 免装 Rust/系统依赖」,采用官方容器镜像 + Compose 编排;纯 CLI 场景用前三行任一即可,命令面与本文镜像内完全一致。
二、镜像选型说明
官方镜像发布在 GitHub Container Registry,tag 与 GitHub Releases 一一对应(如 0.39.0、latest)。生产部署应钉住具体版本 tag(本文 0.39.0),避免 latest 漂移。镜像内部结构实测:
docker pull ghcr.io/cooklang/cookcli:0.39.0
# 0.39.0: Pulling from cooklang/cookcli
# Digest: sha256:fc4943d8c127f76eb65fddf85a17fed39cfa317d40fbce2f759cd175bf6f62a0
# Status: Downloaded newer image for ghcr.io/cooklang/cookcli:0.39.0
docker image inspect ghcr.io/cooklang/cookcli:0.39.0 \
--format '{{.Config.Entrypoint}} {{.Config.Cmd}} user={{.Config.User}} exposed={{json .Config.ExposedPorts}}'
# [/usr/local/bin/docker-entrypoint.sh] [cook server /recipes --host] user=cookcli exposed={"9080/tcp":{}}
关键事实(决定本文所有配置写法):
- 默认命令是
cook server /recipes --host——镜像开箱即用即起 Web 服务,食谱目录固定为容器内/recipes,且已带--host(接受非 localhost 连接)。 - 运行用户是
cookcli(UID/GID 1000),镜像内该用户没有 home 目录——所以登录用户文件、会话密钥必须通过COOK_CONFIG_DIR指到挂载卷,否则无处落盘(见 4.4 与「常见问题」)。 - 入口脚本原样透传参数:
docker compose run --rm cookcli cook <任意子命令>可直接借用镜像内的 cook 二进制操作挂载目录,无需另装 CLI。
UID 实测:
docker compose run --rm cookcli sh -c 'id'
# uid=1000(cookcli) gid=1000(cookcli) groups=1000(cookcli)
三、前置条件
- Docker 引擎与 Compose v2+ 已安装(见第一章回显);
- 宿主机端口 8138 空闲(
ss -ltn | grep 8138无输出); - 食谱库来源二选一:自有 Git 仓库 clone 到
recipes/(CookCLI 数据即纯文本文件,天然适合 Git 管理),或先用cook seed生成官方示例库(本文 4.2 实测后者); - 若从官方源拉取
ghcr.io缓慢,可为 Docker 配置代理或镜像加速后重试(本次实测直连拉取成功,未降级)。
四、部署步骤
4.1 创建项目目录并设置属主
镜像以 UID 1000 运行,宿主目录若属 root 则容器写不进去(4.2 的实测报错与修复见「常见问题」第 2 条),先建目录并 chown:
mkdir -p /opt/cookcli/recipes /opt/cookcli/config
chown -R 1000:1000 /opt/cookcli/recipes /opt/cookcli/config
cd /opt/cookcli
4.2 编写 docker-compose.yml
name: cook-0390-stack
services:
cookcli:
image: ghcr.io/cooklang/cookcli:0.39.0
container_name: cookcli-0.39.0-server
restart: unless-stopped
environment:
COOK_CONFIG_DIR: /config
ports:
- "8138:9080"
volumes:
- ./recipes:/recipes
- ./config:/config
networks:
- cook-net
networks:
cook-net:
driver: bridge
ipam:
config:
- subnet: 10.89.59.0/24
逐项说明:
name:显式项目名,防止与其他栈的默认目录名冲突导致down互删;COOK_CONFIG_DIR: /config:把全局配置目录指进挂载卷,users.toml、会话密钥auth-secret都落在这里,重建服务不丢登录态;./recipes:/recipes:食谱库目录,对应镜像默认命令的/recipes;./config:/config:配置目录(users.toml / auth-secret / revoked-sessions);ports:服务内固定 9080(镜像 ExposedPorts),宿主端口按空闲情况选取;subnet:显式网段便于与其他栈隔离排查,可省略由 Docker 自动分配。
4.3 初始化示例食谱库
自有食谱则 git clone 到 ./recipes 后跳过本节。用镜像内 cook 直接 seed:
docker compose run --rm cookcli cook seed /recipes
ls recipes/
# 2 Day Plan.menu Breakfast config db lamb-chops.cook Neapolitan Pizza.cook
# README.md reports Risotto.cook Salads Shared Thai Green Curry.cook Weekly Plan.menu
seed 一次铺满全套功能素材:12 份 .cook 食谱、2 份 .menu 菜单、config/(aisle.conf + pantry.conf 示例)、db/(购物清单元数据)、reports/(Jinja2 报表模板)、示例图片。验证服务可解析(注意 compose run 的工作目录是 / 而非食谱目录,相对路径会报 Recipe not found,需带 /recipes 前缀或 -b /recipes):
docker compose run --rm cookcli cook recipe "/recipes/Neapolitan Pizza.cook" | head -5
# Neapolitan Pizza
# #italian #pizza
#
# source: https://www.stadlermade.com/how-to-pizza-dough/neapolitan/
# time: 20m
4.4 创建第一个登录用户(开启签名登录)
无 users 文件时 --host 服务对任何可达者开放改库权限,务必先建用户再启动。user add 交互式提示输入两次密码(示例账号 admin / q1w2e3r4);非终端环境从 stdin 首行读取,可脚本化:
printf 'q1w2e3r4\n' | docker compose run --rm cookcli cook server user add admin
# Added admin to /config/users.toml as admin
# Restart cook server to turn sign-in on.
回显第二行是官方明确提示:签名登录在服务「启动时」检测到 users 文件才生效,所以正确顺序是先建用户、后 up -d(若服务已在跑,则需重启一次)。
查看用户与角色:
docker compose run --rm cookcli cook server user list
# admin admin
users.toml 落盘内容(argon2id 哈希,非明文密码):
cat config/users.toml
# [users]
# admin = "$argon2id$v=19$m=19456,t=2,p=1$..."
💡 技术拓展:口令存储算法不止 argon2 一种。常见方案对比——
算法 特点 现状 argon2id 内存硬,抗 GPU/ASIC 破解,PHC 2015 冠军 OWASP 首选推荐,CookCLI 所用 bcrypt 老牌、实现成熟,对 GPU 防御弱于 argon2 仍可用,遗留系统多 scrypt 内存硬思路早于 argon2 参数调优繁琐 PBKDF2 纯计算迭代,HSM/合规场景常见 FIPS 环境首选 明文/可逆加密 — 禁止 CookCLI 固定使用 argon2id(
m=19456,t=2,p=1),手写users.toml时可用cook server hash-password生成哈希,见第八章。
4.5 启动服务
docker compose up -d
# Network cook-0390-stack_cook-net Creating
# Container cookcli-0.39.0-server Creating
# Container cookcli-0.39.0-server Created
# Container cookcli-0.39.0-server Starting
# Container cookcli-0.39.0-server Started
docker compose ps
# NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
# cookcli-0.39.0-server ghcr.io/cooklang/cookcli:0.39.0 "/usr/local/bin/dock…" cookcli 3 seconds ago Up 3 seconds 0.0.0.0:8138->9080/tcp, [::]:8138->9080/tcp
启动日志确认三行关键状态:
docker compose logs
# cookcli-0.39.0-server | Listening on http://[::]:9080
# cookcli-0.39.0-server | Serving recipe files from: "/recipes"
# cookcli-0.39.0-server | Sign-in: on, 1 user(s) in /config/users.toml. Guests can browse but not make changes.
Sign-in: on 一行即 4.4 建的用户已生效:游客可浏览,改动必须登录。
4.6 部署验证
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8138/
# 200
curl -s http://127.0.0.1:8138/ | grep -o '<title>[^<]*</title>'
# <title>Recipes - Cook</title>
curl -s http://127.0.0.1:8138/api/stats
# {"recipe_count":12,"menu_count":2,"pantry_item_count":29,"pantry_expiring_count":1,"pantry_depleted_count":0}
写权限闭环验证——未登录写操作应被拒,登录后应成功:
# 未登录 PUT(期望 401)
curl -s -o /dev/null -w '%{http_code}\n' -X PUT http://127.0.0.1:8138/api/recipes/Test.cook --data 'x'
# 401
# 登录取会话 cookie(/login 表单,303 跳转即成功)
curl -s -c jar.txt -o /dev/null -w '%{http_code}\n' \
-d 'username=admin' --data-urlencode 'password=q1w2e3r4' http://127.0.0.1:8138/login
# 303
# 带会话写入食谱(200)
curl -s -b jar.txt -X PUT http://127.0.0.1:8138/api/recipes/Test.cook --data 'Test recipe
- {1%cup} water'
# {"path":"Test.cook","status":"success"}
# 读回原文(200)
curl -s -b jar.txt http://127.0.0.1:8138/api/recipes/raw/Test.cook
# Test recipe
# - {1%cup} water
# 清理测试食谱(200)
curl -s -b jar.txt -o /dev/null -w '%{http_code}\n' -X DELETE http://127.0.0.1:8138/api/recipes/Test.cook
# 200
浏览器访问 http://<服务器IP>:8138 可见食谱列表、搜索、购物清单、库存页;用 admin / q1w2e3r4 登录后出现编辑与新增控件。活动日志同步落服务日志(实测样例):
2026-11-01 11:25:16 admin created recipe "Test.cook"
2026-11-01 11:25:16 admin deleted recipe "Test.cook"
2026-11-01 11:25:34 admin added "Neapolitan Pizza.cook" to the shopping list
五、数据持久化说明
CookCLI 无数据库,全部状态都是 ./recipes 与 ./config 两个目录里的纯文本文件,备份 = 打包这两个目录:
| 宿主路径 | 服务内路径 | 内容 | 写入者 |
|---|---|---|---|
./recipes/ | /recipes | .cook 食谱、.menu 菜单、图片、config/aisle.conf、config/pantry.conf、.shopping-list、.shopping-checked、db/、reports/ | Web 编辑、CLI、cook seed |
./config/ | /config | users.toml(用户与角色)、auth-secret(会话签名密钥,自动生成)、revoked-sessions(登出会话记录) | cook server user 命令、服务自身 |
要点:
- 食谱与库存的改动直接写回宿主文件,Git 可管版本;服务不跟出食谱目录的符号链接,安全边界清晰;
auth-secret删除并重启会全员登出;users.toml删除并重启则签名登录整体关闭;- 购物清单持久化在食谱库根部的
.shopping-list/.shopping-checked两个隐藏文件里(/api/stats的 pantry 计数即来自config/pantry.conf)。
六、环境变量表
官方镜像支持的配置注入方式(完整参数优先级:命令行 flag > 环境变量 > 默认值):
| 变量 | 取值 | 作用 | 备注 |
|---|---|---|---|
COOK_CONFIG_DIR | 目录路径 | 全局配置目录:users.toml、auth-secret、revoked-sessions、aisle.conf/pantry.conf 回退位置、CookCloud 会话与同步库 | 镜像内用户无 home,必须设置并指向可写挂载卷 |
COOK_USERS_FILE | 文件路径 | 指定 users 文件位置,取代配置目录内的 users.toml | --users-file flag 优先级更高 |
COOK_CORS_ORIGIN | 逗号分隔 origin | 声明允许跨域读写的浏览器 origin,同时决定服务应答哪些主机名 | 专为容器设计——加 flag 意味着重抄整条镜像命令;--cors-origin 覆盖它 |
COOK_RECIPES_ONLY | 1 / true / yes | 未登录访客只见食谱(无购物清单/库存/编辑器) | 等价 --recipes-only |
空值一律按「未设置」处理。除上表外其余服务器选项没有环境变量形态,--no-csrf-check 只能走命令行。
七、服务器配置参数说明
镜像默认命令 cook server /recipes --host 之外,可在 compose 里加 command: 覆盖整条命令。核心参数实测自 docker compose run --rm cookcli cook server --help:
| 参数 | 默认值 | 说明 |
|---|---|---|
[BASE_PATH] | 当前目录 | 食谱库根目录,镜像默认 /recipes |
--host [<ADDRESS>] | 仅 localhost | 允许外部主机连接;带地址则只绑定该地址。非受信网络慎用 |
-p, --port <PORT> | 9080 | 监听端口(服务内),compose 的宿主映射端口独立选择 |
--url-prefix <PREFIX> | 无 | 反代子路径部署,如 /cook,所有路由/链接/cookie 带前缀 |
--proxy-strips-prefix | 关 | 反代转发前剥掉前缀时用(Traefik StripPrefix、nginx 尾部 / 的 proxy_pass、HA ingress) |
--cors-origin <ORIGIN> | 无 | 允许跨域读写的 origin,可重复;* 仅放开读、写仍 403 |
--cors-allow-credentials | 关 | 跨域请求携带 cookie,需至少一个显式 origin |
--no-csrf-check | 关 | 关闭同源强化(Host 检查/写请求/编辑器 LSP),仅当反代改写 Host 且无法用 --cors-origin 表达时用 |
--max-lsp-sessions <N> | 8 | 编辑器语言服务器并发上限,超出回 503;公网服务建议 0 关闭 |
--users-file <PATH> | 配置目录 users.toml | 登录用户文件 |
--recipes-only | 关 | 访客只读食谱模式 |
反代场景的推荐组合(官方文档实测口径):TLS 终结在反代、按域名访问时,COOK_CORS_ORIGIN: https://cook.example.com 即可,无需改命令;若反代剥前缀再加 --url-prefix /cook --proxy-strips-prefix。检查读真实 Host 头而忽略 X-Forwarded-Host,HTTPS 需反代发 X-Forwarded-Proto: https 使 cookie 带 Secure。
八、配置文件挂载
users.toml 支持两种条目形态(cook server user add 自动维护,手写时管理员用裸哈希、其他角色用表):
# Who may change recipes on this server.
[users]
admin = "$argon2id$v=19$m=19456,t=2,p=1$..."
demo = { hash = "$argon2id$v=19$m=19456,t=2,p=1$...", role = "reader" }
角色权限阶梯(每级含上级全部能力):
| 角色 | 购物清单与库存 | 食谱/菜单增删改与图片 | CookCloud 同步 |
|---|---|---|---|
reader | |||
shopper | ✅ | ||
editor | ✅ | ✅ | |
admin | ✅ | ✅ | ✅ |
手写哈希的生成命令:
docker compose run --rm cookcli cook server hash-password
# (交互输入口令)$argon2id$v=19$m=19456,t=2,p=1$ZmzR3LJqY8rYvqaQDbrs1A$YZOAdeeLuJUQSqEX1C0Qw97o5DXNzN7gWEYTqPe1CmM
角色变更无需重启、即时生效(实测):
printf 'q1w2e3r4\n' | docker compose run --rm cookcli cook server user add demo --role reader
# Added demo to /config/users.toml as reader
docker compose run --rm cookcli cook server user list
# admin admin
# demo reader
docker compose run --rm cookcli cook server user remove demo
# Removed demo from /config/users.toml
食谱库内的两份配置文件随 ./recipes 一起挂载:recipes/config/aisle.conf(购物清单分区)与 recipes/config/pantry.conf(库存,TOML),查找顺序为「食谱目录 ./config/ 优先,全局配置目录回退」,参数级详解见本专栏配置篇。
九、等价 docker run
Compose 定义展开为单条 run 命令(实测可用,验证后已删除该测试实例):
docker run -d --name cookcli-run \
-p 8139:9080 \
-e COOK_CONFIG_DIR=/config \
-v /opt/cookcli/recipes:/recipes \
-v /opt/cookcli/config:/config \
--restart unless-stopped \
ghcr.io/cooklang/cookcli:0.39.0
# 92c108c6fddb88134d9ad4324015724291f8187ccd2309a5bcae01aa75d60bf6
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8139/
# 200
docker rm -f cookcli-run
需要覆盖默认命令时,在镜像名后追加完整 cook server ... 参数(会替换 cook server /recipes --host,/recipes 路径要自己带上)。
十、常用运维命令
# 状态与端口
docker compose ps
# 跟踪日志(活动日志:谁在何时改了什么)
docker compose logs -f --tail 50
# 改配置/换版本 tag 后滚动生效
docker compose up -d
# 重启(users.toml 首建、auth-secret 轮换都需要它)
docker compose restart
# 借用镜像内 cook 跑任意 CLI(读写挂载目录,与宿主 CLI 等价)
docker compose run --rm cookcli cook --version
# cookcli 0.39.0 - in food we trust
# 升级:改 image tag 后 up -d
docker compose up -d # image: ghcr.io/cooklang/cookcli:<新版本>
# 停止并移除服务(数据目录不受影响)
docker compose down
十一、常见问题
Q1:浏览器用主机名(如 http://nas.local:8138)打开页面,所有请求 403?
服务默认只应答 localhost、IP 地址或 --cors-origin 声明过的主机名——防 DNS rebinding。实测 403 响应体直接给出解法:
{"error":"Only localhost and IP addresses are trusted as this server's own address, since another site could point any other host name at it (DNS rebinding). If you opened the web UI at http://evil.example.com, start the server with --cors-origin http://evil.example.com, or with --no-csrf-check to disable this check."}
容器内用环境变量补声明(compose environment: 加 COOK_CORS_ORIGIN: http://nas.local:8138)后 docker compose up -d 重建即可;日志同样会 WARN 提示该 flag。只按 IP 访问则无需处理。
Q2:cook seed 或建用户报 Permission denied (os error 13)?
宿主目录属 root 而服务以 UID 1000 运行。实测报错:
Error: Failed to write seed file: /seed/2 Day Plan.menu
Caused by:
Permission denied (os error 13)
修复即 4.1 的 chown:chown -R 1000:1000 recipes config。注意「先 mkdir 再 chown 再跑」,若目录由 Docker 自动创建则属 root,必踩此坑。宿主 UID 恰为 1000 时可加 user: 映射,否则保持默认。
Q3:加了第一个用户但登录框不出现/写操作仍不拦截?
签名登录只在服务启动时检测 users 文件。user add 回显已提示 Restart cook server to turn sign-in on.——docker compose restart 一次即可。后续增删用户、改角色则热生效,无需重启。
Q4:Releases 页的 .sha256 文件用 sha256sum -c 校验失败?
实测:sha256sum: cook-0.39.0-linux-x86_64-musl.tar.gz.sha256: no properly formatted checksum lines found——该文件只含裸哈希值,不含「哈希 + 文件名」两列格式。正确用法:
sha256sum cook-0.39.0-linux-x86_64-musl.tar.gz
# 9c4c8732f3ac3adbf13f81751ca2fa87e290e6a63745c5fbf5efd8815065e4f9
# 与 .sha256 文件内容人工比对
Q5:docker compose down 误删了别的组件的服务?
compose 默认项目名取目录名,不同目录同名即互撞。本文 compose 已显式 name: cook-0390-stack 规避;自建 compose 部署 CookCLI 时务必同样显式指定项目名。
十二、参考链接
- CookCLI 仓库与 Releases:https://github.com/cooklang/cookcli
- 官方命令文档(server):https://cooklang.org/cli/
- 官方镜像:
ghcr.io/cooklang/cookcli - 本专栏 《CookCLI 介绍与选型指南》