Dokploy Schedules 踩坑:搞懂三個排程 tab,順便繞過一個遠端 server 的 dockerode bug
起源
剛開始用 Dokploy 管理一堆 self-hosted 服務時,每個 service 頁面上有三個跟「定時」有關的 tab:
?tab=backups
?tab=schedules
?tab=volumeBackups
新手第一次看一定霧煞煞——這三個東西到底差在哪?我以為 Schedules 就是「設定什麼時候跑備份」,結果完全不是。釐清過程中還順便踩到一個遠端 server 才會出現的雷,記錄下來給後人參考。
三個 tab 在做什麼
它們完全獨立、互不相干,只是名字都跟「定時」有關。
| Tab | 在做什麼 | 救得回 | 救不回 |
|---|---|---|---|
| Backups | 定時跑 pg_dump / mysqldump 把 DB 內容倒成檔,上傳 S3 |
DB 內容(資料、設定) | DB binary、檔案 volume、log |
| Volume Backups | 把 docker volume 整顆打 tar.gz 上 S3 | 任何純檔案類資料 | 運行中的 DB(會抓到中間狀態,可能壞) |
| Schedules | 到指定時間在容器/host 內跑一行 shell 指令 | 跟備份完全無關——是 cron job | — |
重點:Backup 的「什麼時候跑」寫在 Backup 自己裡面(每筆 Backup 都有一個 cron 欄位,例如 0 4 * * 0)。Schedules tab 是另一個完全獨立的功能,用來定時跑你自己想跑的指令。
Schedules 能做什麼
凡是「定時跑一行指令」的需求都可以放這。一些例子:
1. DB 健康維護
Postgres 跑久了會累積 dead tuple,定期 VACUUM 可以維持效能:
docker exec myapp-postgres-1 psql -U myuser -d mydb -c "VACUUM ANALYZE;"
Code language: JavaScript (javascript)2. 容器內暫存清理
find /app/tmp -type f -mtime +7 -delete
Code language: JavaScript (javascript)3. 健康檢查 + 通報
curl -fsS --max-time 2 http://myapp:3000/ \
|| curl -X POST "https://api.telegram.org/bot<TOKEN>/sendMessage" \
-d chat_id=<YOUR_ID> -d "text=myapp 掛了"
Code language: JavaScript (javascript)4. 第三方服務的 GC
例如 MLflow、Sentry 之類有自己 GC 指令的服務。
什麼時候**不**該用 Dokploy Schedules
- 業務邏輯(爬蟲、ETL、報表)→ 用該領域的 orchestrator(例如 Airflow / Dagster / Prefect)
- 需要看血緣、retry、依賴關係 → 同上
- 「失敗了客戶會哀號」的事 → 同上,因為 Dokploy Schedules 沒有好的失敗通知機制
判斷口訣:這件事如果連續失敗 3 天我會不會痛?
- 會痛 → 不要放 Dokploy Schedules
- 不會痛 → Dokploy Schedules 很方便
踩到的雷:scheduleType=compose 對遠端 server 會壞
症狀
我建了一個 schedule 給某個 compose service:
scheduleType: composeserviceName: postgres(compose 裡的服務名)command: psql -U myuser -d mydb -c "VACUUM ANALYZE;"- cron:
0 3 * * 0
UI 顯示 200 OK,但 manual run 立刻 500,Dokploy 容器 log 出現:
The URL ssh:192.168.50.10:22192.168.50.10 is invalid
Execution error: Error: No response from server
Code language: JavaScript (javascript)注意那個 URL:ssh:<host>:<port><host>——port 跟 host 黏在一起了,看起來像 dockerode 在組 SSH URL 時的 bug。
為什麼會壞
挖了 Dokploy 源碼後發現執行流程是:
schedule.runManually
→ runCommand
→ getComposeContainer(compose, serviceName)
→ getRemoteDocker(serverId)
→ new Dockerode({ protocol: "ssh", host, port, username, sshOptions })
Code language: CSS (css)getComposeContainer() 為了拿 container ID,用 dockerode 的 SSH protocol 直接連遠端 docker daemon。而 dockerode 的 SSH protocol 在現行版本有 URL 組成 bug,連不上 → No response from server。
對照之下,下列兩條路完全沒問題:
scheduleType=server的執行路徑 → 走execAsyncRemote()(純 ssh2 spawn shell)- Dokploy UI 的 container list → 走
docker psover SSH(也是純 ssh2)
問題只出在「dockerode 透過 SSH 直連 docker daemon」這條路。
繞過方法:改用 scheduleType=server + script
不要讓 Dokploy 用 dockerode SSH 找 container,自己在 script 裡呼叫 docker exec 就好:
#!/bin/bash
set -e
docker exec myapp-postgres-1 psql -U myuser -d mydb -c "VACUUM ANALYZE;"
Code language: JavaScript (javascript)對應的 schedule 設定:
| 欄位 | 值 |
|---|---|
scheduleType |
server |
serverId |
你的遠端 server ID |
composeId |
(空) |
serviceName |
(空) |
script |
上面那段 bash |
cronExpression |
0 3 * * 0 |
Dokploy 會把這份 script 寫到遠端 server 的 /etc/dokploy/schedules/<appName>/script.sh,cron 觸發時 SSH 過去 bash script.sh。執行 log 在同個目錄下的 *.log。
用 MCP / API 怎麼建
如果你用 Dokploy MCP 或 API 建(不從 UI),呼叫 schedule-create 時注意:
{
"name": "DB Vacuum",
"cronExpression": "0 3 * * 0",
"scheduleType": "server",
"serverId": "<your server id>",
"script": "#!/bin/bash\nset -e\ndocker exec myapp-postgres-1 psql -U myuser -d mydb -c \"VACUUM ANALYZE;\"\n",
"shellType": "bash",
"enabled": true,
"command": ""
}
Code language: JSON / JSON with Comments (json)command 欄位給 server-type 用不到,但 schema 可能要求填字串,給空字串即可。
UI 位置
注意:改成 scheduleType=server 後,這筆 schedule 不會出現在原本 compose service 頁面的 Schedules tab,而是在:
Settings → Servers → 你的 server → Schedules tab
因為它現在掛在 server 底下、不掛在 service 底下。
小結
- Dokploy 三個 tab 完全獨立:Backups 備 DB、Volume Backups 備檔案、Schedules 跑 cron 指令。
- Backups 的排程寫在 Backup 自己,不是 Schedules tab。
scheduleType=compose對遠端 server 會踩 dockerode SSH bug,改用scheduleType=server+ script 裡docker exec就好。- 本機 dokploy(service 跑在 dokploy 自己機器上)的 compose-type schedule 沒這問題;只有跨機器才會踩。
希望這篇能省下後人挖源碼的時間。