容器 App
容器 App 是打包成 DSM 套件的 Compose 專案。你保留原本的 compose.yaml,再加上一個小小的 synology-app.yaml。synopkgland build 會產生 SPK,透過套件中心安裝後,由 Container Manager 執行你的容器。如果 app 有 Web 介面,DSM 使用者會從 DSM 桌面開啟,並先經過 gateway 檢查 DSM 工作階段。
先從 headless 範例開始。它沒有 Web 連接埠,只有一個共用資料夾和一個 DSM 使用者:
synopkgland init -t compose my-app要打包現有的 app,請匯入它的 Compose 檔。init 會把它複製成 compose.yaml,並產生一份讓你接著編輯的 synology-app.yaml:
synopkgland init -t compose --from ./compose.yaml my-apppackage 與 displayname 取自 --package、--name 或目錄名稱。install 只保留匯入檔案有引用其變數的模板項目。接著設定 description 和 maintainer;如果 app 有 Web 介面,加上 adminport;app 需要的其他共用資料夾或設定,都要再加一個 install 項目。請參考欄位參考。
每個 image 都要用 digest 固定。images pin 以匿名 registry HTTP 解析每個 tag,所以私有 registry 會失敗:
synopkgland images pin下面這組檔案是帶有安裝設定的 Web app,可通過 check 與 build:
services: web: image: traefik/whoami:v1.10.3@sha256:43a68d10b9dfcfc3ffbfe4dd42100dc9aeaf29b3a5636c856337a5940f1b4f1c command: ["--port=8080"] environment: GREETING: "${GREETING}" ports: - "127.0.0.1:18080:8080" volumes: - "${SYNOPKG_PKGVAR}/data:/data"schema: 1package: hello.whoamiversion: 0.1.0displayname: Hello Whoamidescription: A small web service behind the DSM gateway.maintainer: Example Developeros_min_ver: "7.2.1-69057"adminport: 18080adminurl: /install: GREETING: type: string default: hellointegration: web: launch: dsm-window access: dsm-users建置與測試:
synopkgland checksynopkgland build| 指令 | 作用 |
|---|---|
check | 驗證 synology-app.yaml 與 compose.yaml,並列出偵測到的服務、發佈的連接埠和資料夾。 |
build | 產生 SPK。Web app 在未指定 --arch 時會建置 <package>-x86_64.spk 與 <package>-aarch64.spk;headless app 只產生一個 <package>.spk。-o 需要搭配單一 --arch。 |
dev | 驗證後執行 docker compose up -d,等服務健康(沒有 healthcheck 時則等到執行中),按 Ctrl+C 時停止服務。 |
test | 驗證後,在 .synopkgland/ 下準備 ${SYNOPKG_PKGVAR} 與 secrets 資料夾,執行 up 並等待就緒。容器會繼續執行。 |
deploy | 建置 SPK 並安裝到 NAS。--share、--run-as 與 --param 請見 deploy --help。 |
dev 與 test 需要你的電腦安裝 Docker。
Web 入口
Section titled “Web 入口”把 adminport 設成容器發佈的主機連接埠,套件就會有一個 gateway:DSM 開啟 /webman/3rdparty/<package>/cgi/gateway.cgi/<adminurl>,gateway 檢查 DSM 工作階段與你的存取規則,再把請求轉給容器。
請像範例一樣只在 loopback 發佈 Web 連接埠(127.0.0.1:18080:8080)。gateway 是唯一預期的入口。如果 adminport 的主機連接埠監聽在其他位址,check 會以 COMPOSE_GATEWAY_PORT_EXPOSED 失敗。其他在所有介面發佈的連接埠會列在 check 報告中,而 inspect 會對它們失敗。NAS 上的其他程序仍可連到 loopback 連接埠,所以不要把 X-Syno-User 用在本機程序不該做的事情上。
經過 gateway 後,你的 app 會看到這些變化:
- gateway 設定
X-Syno-User(DSM 使用者名稱)與X-Syno-Admin(true或false)。 - 請求中的
Cookie、Remote-*、Forwarded、X-Syno-*、X-Forwarded-*、X-Remote-*、X-Auth*與X-Real-*會被丟棄,所以integration.web.cookies在 gateway 後面沒有作用。 - 回應中的
Set-Cookie會被移除,Cache-Control強制為private,並加上Vary: Cookie。 Location標頭會改寫到 gateway 路徑底下。- WebSocket 與 Server-Sent Events 可直接通過。WebSocket 請求中,你的 app 看到的是瀏覽器的
Host。
啟動模式與存取
Section titled “啟動模式與存取”| 設定 | 值 | 結果 |
|---|---|---|
integration.web.launch | dsm-window(預設)、new-page | dsm-window 會加入 DSM 主選單與桌面項目,點擊後在新的瀏覽器分頁開啟 app。new-page 不加項目,由套件中心的開啟在新分頁開啟 app。 |
integration.web.access | dsm-users(預設) | 任何已登入的 DSM 使用者。 |
dsm-admins | administrators 群組成員。 | |
dsm-groups | integration.web.groups 中任一群組的成員。 | |
public | 所有人,不需登入。套件使用 Web Station 而不是 gateway。 |
群組會逐字比對。本機群組直接寫名稱,例如 family;目錄群組寫成 'DOMAIN\group' 或 group@domain。反斜線的寫法在 YAML 中要加引號。
integration: web: access: dsm-groups groups: [family, 'EXAMPLE\Domain Users']Compose App 不能宣告 integration.web.portal:它沒有 executor,而解除安裝後要讓 DSM 網頁伺服器停止提供登入入口網站別名,只有 root 做得到。check 會以 CONTRACT_PORTAL_REQUIRES_EXECUTOR 失敗。需要短網址時,由管理員在 DSM 設定反向代理。
app 必須送出的請求
Section titled “app 必須送出的請求”gateway 把 GET、HEAD 與 OPTIONS 以外的所有方法視為會變更狀態。這些請求需要 X-SYNO-TOKEN 標頭,也必須證明來自 DSM 的來源:Sec-Fetch-Site 若有送出,必須是 same-origin;Origin 若有送出,必須等於 Host。兩個標頭至少要有一個。WebSocket 握手也適用相同的同源規則。
如果你希望 app 自己回的 403、404、500、502、503 或 504 連同內容送到瀏覽器,請在請求上送出 X-Synopkgland-Keep-Error-Body: 1。否則 DSM 的 Web 伺服器會把這些狀態換成它自己的錯誤頁。設定這個標頭後,gateway 會依下表替換狀態,並把真正的狀態放在 X-Synopkgland-Status。
gateway 錯誤
Section titled “gateway 錯誤”每個 gateway 錯誤都有純文字內容(code、換行、訊息),以及 X-Synopkgland-Error: <code> 與 Cache-Control: no-store 標頭。DSM 會取代狀態為 403、404、500、502、503 與 504 的回應,所以 gateway 送出替代狀態,並把原本的狀態放在 X-Synopkgland-Status。請依 X-Synopkgland-Error 判斷。
X-Synopkgland-Error | 送出的狀態 | 原本的狀態 | 原因 |
|---|---|---|---|
session_required | 401 | 401 | 沒有工作階段,或 DSM 拒絕它。 |
forbidden | 401 | 403 | 已登入,但存取規則拒絕該使用者。 |
token_required | 401 | 403 | 會變更狀態的請求沒有可用的 X-SYNO-TOKEN。 |
cross_site_blocked | 401 | 403 | 會變更狀態的請求或 WebSocket 握手不是同源。 |
not_found | 410 | 404 | 請求不在 gateway 路徑底下。 |
bad_path | 400 | 400 | 容器內的路徑不被允許。 |
redirect | 308 | 308 | 補上結尾的斜線。 |
body_too_large | 413 | 413 | 請求內容超過 64 MiB。 |
upgrade_unsupported | 501 | 501 | 升級成 WebSocket 以外的協定。 |
upstream_unavailable | 521 | 503 | 容器已停止或拒絕連線。會送出 Retry-After: 5。 |
upstream_timeout | 524 | 504 | 60 秒內沒有回應標頭,或 3 秒內無法連線。 |
bad_gateway | 520 | 502 | 容器回了無效的回應。 |
auth_unavailable | 520 | 500 | DSM 無法驗證工作階段或群組成員資格。 |
從 DSM 開啟 app
Section titled “從 DSM 開啟 app”DSM 主選單與桌面會在新的瀏覽器分頁開啟 dsm-window app,不是 DSM 視窗。DSM 對所有網址項目都這樣開啟,所以 app 沒有 DSM 視窗,也沒有視窗標題。當 DSM 的Improve protection against cross-site request forgery attacks 設定開啟時,這個分頁不會帶 token,gateway 會回應 session_required。請關閉該設定,或改用 native-go 模板建置 app,它每次呼叫都會帶 token。請見原生 App。
每個 install.<NAME> 項目都會在安裝精靈新增一個欄位。在 compose.yaml 中以 ${NAME} 引用它的值。名稱必須符合 ^[A-Za-z_][A-Za-z0-9_]*$。沒有任何 Compose 檔引用的 string、boolean、shared_folder 或 posix_all_shares 設定,會讓 check 以 INSTALL_PARAMETER_UNREFERENCED 失敗。
type | 精靈欄位 | 說明 |
|---|---|---|
string | 文字欄位,不可空白 | default 最多 256 個字元,不可有控制字元,不可有 ${。 |
boolean | 核取方塊 | deploy --param NAME=true 或 false。 |
shared_folder | 共用資料夾選擇器 | 當作 bind 來源使用,見下方。 |
dsm_user | 使用者選擇器 | 最多一個。套件以該使用者執行每個服務。 |
posix_all_shares | 無 | 掛載所有儲存空間。必須是完整的 bind 來源,不能有後綴。deploy 會從管理員的共用資料夾清單自動填入。 |
secret、homes | check 會警告,deploy 會拒絕。 |
bind 共用資料夾時,來源以 ${NAME} 開頭,後面可接子路徑。請在共用資料夾旁宣告 dsm_user,讓容器以 DSM 使用者而不是 root 執行。沒有宣告時,check 會警告(DSM_USER_REQUIRED),而 Community 上架會被拒絕:
install: MEDIA: type: shared_folder RUN_AS: type: dsm_userservices: web: volumes: - "${MEDIA}/photos:/photos:ro"建置時會替每個服務加上 user: "${RUN_AS_UID}:${RUN_AS_GID}" 與該使用者的群組。宣告了 dsm_user 後,不要自己設定 user:。使用 deploy --share MEDIA=<共用資料夾名稱> 與 --run-as RUN_AS=<DSM 使用者>,請給共用資料夾名稱與使用者名稱,不要給路徑或 id。
允許的 bind 來源有 ${SYNOPKG_PKGVAR}(app 的資料夾)、${SYNO_APP_SECRETS_DIR},以及類型為 shared_folder、homes 與 posix_all_shares 的安裝設定。專案內的路徑,例如 ./config,也允許。具名 volume 必須宣告在最上層的 volumes:。tmpfs 允許。${NAME} 後面的子路徑是 / 加上字母、數字、.、_ 與 -。
Compose 中不允許的項目
Section titled “Compose 中不允許的項目”check 會拒絕 compose.yaml 中的這些項目。synology-app.yaml 的 privilege 區塊無法解除其中任何一項。
| Compose 功能 | 錯誤碼 |
|---|---|
build: | COMPOSE_BUILD_FORBIDDEN |
privileged: true | COMPOSE_PRIVILEGED_FORBIDDEN |
network_mode: host | COMPOSE_HOST_NETWORK_FORBIDDEN |
pid: host | COMPOSE_HOST_PID_FORBIDDEN |
ipc: host | COMPOSE_HOST_IPC_FORBIDDEN |
devices:(非空) | COMPOSE_DEVICES_FORBIDDEN |
cap_add:(非空) | COMPOSE_CAP_ADD_FORBIDDEN |
掛載 docker.sock 或 Container Manager socket | COMPOSE_SOCKET_MOUNT_FORBIDDEN |
主機路徑,例如 /volume1/data、~/data 或 ../data | COMPOSE_HOST_BIND_FORBIDDEN |
type: bind 使用不被允許的來源 | COMPOSE_BIND_SOURCE_FORBIDDEN |
${X} bind 來源未在 install 宣告 | COMPOSE_BIND_SOURCE_UNDECLARED |
${X} 後面的子路徑無效 | COMPOSE_BIND_SUFFIX_INVALID |
具名 volume 未宣告在 volumes: | COMPOSE_NAMED_VOLUME_UNDECLARED |
已宣告 dsm_user 又設定 user: | COMPOSE_USER_CONFLICTS_DSM_USER |
有共用資料夾時使用 user: 0、0:0 或 root | COMPOSE_USER_ROOT_FORBIDDEN |
數字的 user:,例如 1000:1000 | COMPOSE_USER_NUMERIC_FORBIDDEN |
command 或 entrypoint 清單中有純數字項目 | COMPOSE_NUMERIC_COMMAND_ARG_FORBIDDEN |
最上層的 secrets: 或 configs:,或未宣告的 ${NAME} | COMPOSE_DYNAMIC_ROUTE_UNSUPPORTED |
depends_on 指向不存在的服務 | COMPOSE_SERVICE_NOT_FOUND |
synology-app.yaml 中的 capabilities: | CAPABILITY_UNSUPPORTED |
COMPOSE_USER_ROOT_FORBIDDEN 在你為自己建置時是警告,在 Community 上架時是錯誤。請見錯誤碼。
| 項目 | 值 |
|---|---|
| 最低 DSM 版本 | 7.2.1-69057(init --min-dsm 與 build --min-dsm 會強制檢查;check 不會) |
| 架構 | x86_64、aarch64 |
| 建置機器 | Web app 需要 Go 1.21 或更新版本(gateway 會固定以 Go 1.26.9 編譯進 SPK) |
package 長度 | 最多 32 個字元;超過 28 個字元會得到 PACKAGE_IDENTITY_TOO_LONG,因為 deploy --dev 會加上 .dev |
| gateway 請求內容 | 64 MiB |
| gateway 等待回應標頭 | 60 秒 |
| gateway 等待連線 | 3 秒 |
| 未用 digest 固定的 image | 會回報為風險;建置時會設定 force_pull: true |
| 症狀或錯誤碼 | 原因 | 解法 |
|---|---|---|
CONTRACT_ADMINPORT_UNMAPPED | adminport 不是已發佈的 TCP 主機連接埠。 | 在 ports: 發佈它,或修正數字。 |
COMPOSE_GATEWAY_PORT_EXPOSED | adminport 的主機連接埠不在 loopback。 | 寫成 127.0.0.1:<port>:<container port>。 |
IMAGE_PIN_REQUIRED | images pin --check 發現未固定或過期的 image。 | 執行 synopkgland images pin。 |
GATEWAY_GO_MISSING | 建置 Web app 需要 Go 1.21 或更新版本。 | 安裝 Go 後重新建置。 |
CAPABILITY_UNSUPPORTED | 容器 App 設定了 capabilities。 | 移除它。 |
CONTRACT_PORTAL_REQUIRES_EXECUTOR | 設定了 integration.web.portal。Compose App 沒有 executor。 | 移除它。可由管理員在 DSM 設定反向代理。 |
COMPOSE_ENV_MISSING | test 無法準備本機資料夾。 | 在專案資料夾中、Docker 執行時再試一次。 |
PORT_ALREADY_IN_USE | deploy 發現發佈的連接埠已被 NAS 佔用。 | 改用其他主機連接埠。 |
瀏覽器顯示 401 與 session_required | 沒有 DSM 工作階段,或上述的 CSRF 設定。 | 重新登入;請見「從 DSM 開啟 app」。 |
瀏覽器顯示 521 | 容器已停止。 | 在套件中心與 Container Manager 檢查套件。 |