撰寫 appcipe.yml
appcipe.yml 是 Chefer 的「食譜」:描述一或多個 Docker/OCI 映像 + 執行參數,
chefer build 會把它們打包成單一執行檔(免裝 Docker/容器引擎,雙擊即跑)。
權威驗證規則在 crates/appcipe-spec/src/validate.rs;完整契約在 docs/DESIGN.md §3/§6;
有註解的範例見 examples/appcipe.yml、可運作的多服務範例見 examples/demo/。
改完一定要 cargo run -p chefer-cli -- check <appcipe.yml> 驗證(會一次列出所有錯誤)。
整體流程(先講給使用者聽)
docker build / docker pull → docker save -o x.tar <image> # 取得 image tar
寫 appcipe.yml(image 指向那些 tar)
chefer build appcipe.yml --out dist # 產出單檔
image 來源可為 tar(docker save/OCI archive)、registry ref(釘版 tag 或 @sha256;公開 image 匿名拉取,私有 registry 用 docker login 的明文 auths 憑證或 CHEFER_REGISTRY_AUTH=user:pass——外部 credential helper 不支援)或 Dockerfile(打包機需有 docker/podman/nerdctl)。
最小可用範例
version: "0.1" # 必填,目前固定 "0.1"
name: MyApp # 必填;[A-Za-z][A-Za-z0-9_-]*,≤64;也是輸出檔名與資料夾名
services:
web:
image: ./images/web.tar # 短寫=tar 路徑(相對 appcipe.yml)
ports: ["8080:8080"] # host:guest,從 host 連 127.0.0.1:8080
頂層欄位
| 欄位 | 必填 | 規則/用途 |
|---|---|---|
version |
✓ | 固定 "0.1"。 |
name |
✓ | [A-Za-z][A-Za-z0-9_-]*,≤64。輸出檔名 <name>_<target>[.exe]、資料目錄名。 |
app_version |
純顯示/中繼資料:check/build/inspect 會印、執行時 log 一次。不影響打包,容器內程式讀不到。要在程式裡用版本就自己塞進服務的 env:。 |
|
old_names |
字串清單,每項須是單一目錄名(同 name 規則,不可含 / \ : .. 或絕對路徑)。data dir 不存在時,依序找舊名目錄自動改名遷移。 |
|
data_dir |
覆蓋持久化資料的父目錄;未設用平台預設(Win %LOCALAPPDATA%\{name}、mac ~/Library/Application Support/{name}、Linux $XDG_DATA_HOME 或 ~/.local/share/{name})。 |
|
crash |
目前僅 fail_fast(任一服務非 0 退出→整個 app 以該碼退出)。舊欄位名 crash_policy 仍接受。 |
|
network |
bridge(預設)|internal|shared。bridge=app 專屬網路、只開宣告的 ports、可出網;internal=同 bridge 但無對外網路;shared=共用 host 網路(不隔離、舊行為)。見下方「內部網路」。 |
|
console |
auto(預設)|shown|hidden。共用主控台(彙整日誌 + Ctrl+C 的終端視窗)顯示策略。auto=有 terminal/both→顯示、只有 gui→隱藏、全無介面→顯示。hidden 僅當 app 另有 gui 或 terminal/both 可關閉才允許(全無介面用 hidden 會驗證失敗)。隱藏只在 Windows 雙擊啟動且獨佔該 console 時生效。 |
|
services |
✓ | 服務字典,見下。 |
services. 欄位
service 名規則:[a-z][a-z0-9_]*,≤32。
image(必填)。寫法:- 短寫:
image: ./x.tar(tar 路徑)或image: redis:7.2-alpine(registry ref,自動判別)。 - 完整(
source三選一):image: source: tar # tar | image | dockerfile file: ./images/x.tar # tar=tar 路徑;image=registry ref(須釘版、非 latest);dockerfile=Dockerfile 路徑 format: docker-archive # auto | docker-archive | oci-archive(**用連字號**,非底線;僅 source=tar 有意義) platform: linux/amd64 # linux/amd64 | linux/arm64(windows/amd64 不支援執行) source: dockerfile另可加context:(build context 目錄,省略 = Dockerfile 所在目錄)與build_args: { K: V }。需打包機上有 docker/podman/nerdctl;不保證可重現(要可重現用source: image釘@sha256digest)。執行期仍不需 Docker。
- 短寫:
cmd:字串或陣列,覆蓋映像的 CMD(不覆蓋 ENTRYPOINT)。有效命令 = entrypoint + (cmd 或 image CMD)。workdir:容器內工作目錄。env:key: value;key 須符合[A-Za-z_][A-Za-z0-9_]*。會覆蓋映像自帶的同名 env。persist_path:要持久化的容器內絕對路徑(須以/開頭)。實體落在{data_dir 或預設}/{name}/data/{service}/,跨重啟保留。沒設=退出即清空。ports:["host:guest[/proto]"](proto 預設 tcp)。同一 app 內 host 埠不可重複。 只有列在這裡的埠才會被 chefer 代理到 host。mounts:["<host路徑>:<容器內絕對路徑>"];容器內路徑須以/開頭;host 路徑在 build 時須存在。interface_mode:gui | terminal | both | none(預設 none)。全 app 最多一個 terminal/both。depends_on:服務名清單。決定啟動順序;若被依賴的服務有healthcheck,依賴者會等它 healthy 才啟動(wait-until-ready),否則等它 spawn 即可。須指向存在的服務、不可循環、不可自指。healthcheck(選填,對齊 Docker HEALTHCHECK):test:命令字串(=sh -c)或陣列(["CMD", ...]直接 argv;["CMD-SHELL", "..."]走sh -c)。在容器內執行,exit 0 = 健康。例:["CMD", "redis-cli", "ping"]、["CMD", "pg_isready", "-U", "postgres"]。interval(預設2s)、timeout(預設5s)、start_period(預設0s):接受<n>ms/<n>s/<n>m或裸整數(秒)。retries(預設10):連續失敗幾次(扣除 start_period 寬限)才算 unhealthy → fail_fast 拆掉整個 app。- 啟動目前序列化:一個服務的 healthcheck 會擋住其後所有服務(含不相依者),通常無感;之後會改成只擋實際 dependents。
gpu(選填,預設false):opt-in GPU passthrough。可寫false(關)/true(全部 GPU)/[0, 2](只綁指定 NVIDIA 卡,硬隔離);也接受字串all/none(Docker--gpus all慣用別名,等同true/false)。開啟後 guest-agent 把 host GPU 裝置節點(/dev/nvidia*、/dev/dri、AMD 的/dev/kfd、WSL2 的/dev/dxg)bind 進這個服務的容器 → CUDA/ROCm/OpenCL 計算與 NVENC/NVDEC 視訊。驅動 userspace:原生 NVIDIA 由 chefer 自動注入 host 相符的libcuda/libnvidia-*(image 免自帶);WSL2 由/usr/lib/wsl/lib+/usr/lib/wsl/drivers提供;AMD/Intel 由映像自帶(rocm/*、intel/oneapi)。只在原生 Linux 與 Windows WSL2 後端可行(WHP micro-VM/macOS VM 服務啟動時明確報錯)。gpu: [i,…]卡索引硬隔離僅原生 NVIDIA 有效——WSL2/AMD/Intel 請改用env的CUDA_VISIBLE_DEVICES/HIP_VISIBLE_DEVICES/ZE_AFFINITY_MASK(軟選擇)。只給真的需要 GPU 的服務開。實機驗證:CUDA/PyTorch/NVENC/nvidia-smi(RTX 4070 + WSL2 GT 1030);OpenGL/Vulkan 算繪無頭容器不支援。
內部網路與「不對外暴露」(重要、實測過)
- 整個 app 跑在自己的 network namespace(預設
network: bridge)。服務間互連用127.0.0.1:<port>(例:app 連 db 設env: { DB_HOST: "127.0.0.1", DB_PORT: "6379" })。 - 只有列在某服務
ports:的埠才會被代理到 host;未宣告的埠在bridge/internal下真正不對外 (服務只在 app netns 的lo監聽 → host 連不到、WSL2 wslrelay 也看不到)。所以「不列 ports 的 db」 在預設bridge下確實內部專用。已於原生 Linux 與實機 WSL2 驗證。 - 想讓服務能出網(裝套件、call 外部 API)用預設
bridge;只要服務間互通、不需出網用internal。 - ⚠️ 只有顯式寫
network: shared才回到舊的「共享 host 網路、未宣告埠也可從 host 連到」行為—— 這時才不要向使用者保證 db 不可達。
真實映像的陷阱(實測過)
- 官方 redis/postgres/nginx 等的 entrypoint 常在以 root 執行時
chown+gosu切到 服務專用 uid(如 999)。這類映像在 WSL2、macOS VM、以及原生 Linux 以 root 執行時 可直接使用——這些後端讓服務以真實 root 執行(不開 user namespace),chown/gosu 到任何 uid 都成功(官方redis已在 WSL2 實測通過)。原生 Linux 的 rootless 路徑(以非 root 使用者執行單檔)也支援:host 有newuidmap/newgidmap(uidmap套件)且使用者在/etc/subuid//etc/subgid有委派範圍時(多數發行版預設都有),guest-agent 走範圍映射 (同 rootless podman),chown/gosu 映像照跑(官方redis已以非 root 實測)。只有 無委派環境(無 uidmap 或無 subuid 範圍)會退回單一 uid 映射,那些映像可能chown失敗而起不來——此時裝uidmap並補 subuid 範圍,或改用以容器 root 直接執行、不 chown 的映像(例:自建alpine + apk add redis)。參考examples/demo/db/Dockerfile。 format用連字號:docker-archive/oci-archive(底線形式也接受,但文件統一用連字號)。- registry ref 必須釘版(
redis:7.2-alpine或@sha256:…);latest/未帶 tag 會被驗證拒絕。私有 registry 認證尚未支援,仍可docker pull+docker save走 tar。
多服務範例(app + db,內部連線 + 持久化)
version: "0.1"
name: CheferDemo
app_version: "1.0.0" # 顯示用;要在程式裡用就另外塞 env
services:
db:
image: ./images/db.tar # 建議:自建 alpine+redis(CMD 直接跑、不 chown)
persist_path: /data # redis AOF 持久化到此
interface_mode: none
# 不列 ports:不主動對外(但見上面 v1 netns 缺口說明)
app:
image: ./images/app.tar
env:
DB_HOST: "127.0.0.1" # 內部網路:與 db 共享 localhost
DB_PORT: "6379"
ports: ["18080:8080"] # 唯一對外的埠
interface_mode: none
depends_on: [db] # 先啟動 db(app 仍應自行重試連線)
收尾檢查清單
cargo run -p chefer-cli -- check <appcipe.yml>→ exit 0。- 每個
image指向的 tar 都存在(docker save產生)。 persist_path、mounts的容器內路徑都以/開頭。- host 埠全 app 唯一;只有真要對外的服務才列
ports。 - 跨服務連線用
127.0.0.1:<port>+env,不要假設 DNS 服務名。