部署
Docker image 的啟動語義
目前 Docker image 使用:
ENTRYPOINT ["vylux"]
CMD ["--mode=all"]
代表:
- 映像的預設行為是
all - 但
--mode=all只是預設參數,不是硬限制 - 在 Docker、Compose、K8s 都可以覆蓋
args改成--mode=server或--mode=worker
這樣做的目的,是讓本機與最小部署可以開箱即用;到了正式環境,再依角色拆分。
已發布映像
官方容器映像發布於 ghcr.io/carry0987/vylux:latest。
.github/workflows/docker.yml 內的 release workflow 會透過 GitHub Actions 自動建置並發布這個映像。它會推送版本標籤與 latest,並建立支援下列平台的 multi-platform manifest:
linux/amd64linux/arm64
除非你是在驗證尚未發布的 Dockerfile 變更,否則標準的 Docker Compose 與 Kubernetes 部署都可以直接 pull 這個映像,不需要本機重新 build。
目前已發布映像的 runtime layer 也已透過 Alpine 的 vips-heif 內建 HEIF support。這就是容器部署中 heic、heif、avif 這類 HEIF-family 輸入可用,以及 AVIF 輸出可用的關鍵。
本機建置 repository Dockerfile 時的注意事項
如果你確實需要自行 build repository 內的 Dockerfile,請保留目前 libvips 套件來源的處理方式。
builder stage 會刻意先覆寫 /etc/apk/repositories,只保留 edge/main 與 edge/community,之後才 apk add vips-dev。若把 edge repositories 只是附加到預設 golang:1.27-alpine 的 stable 清單後面,libvips 8.18+ 很容易跟 OpenSSL 相依版本發生衝突。
實務上代表:
- 不要把預設 stable repositories 再加回
vips-dev安裝流程前面 - 若你 fork 了 Dockerfile,請保留 edge-only 的 builder repository 設定
- 若你需要 HEIF-family 輸入或 AVIF 輸出,也不要移除 runtime 內的
vips-heif - 目前 CI 驗證追蹤的 libvips 版本是
8.18.6
本機開發
最常見的本機組合是:
- PostgreSQL via Docker
- Redis via Docker
- RustFS 或其他 S3-compatible storage
- Vylux host process,或直接跑
allmode image
repo 目前其實提供兩種不同的本機流程:
docker-compose.dev.yml:只起基礎設施,Vylux 本體跑在 host 上docker-compose.yml:Vylux、PostgreSQL、Redis 與可選 Cloudflare Tunnel 都跑在容器內
docker compose -f docker-compose.dev.yml up -d
go run ./cmd/vylux
這些部署範例背後對應的完整環境變數、預設值與驗證規則,請見 設定。
執行模式
MODE=all: 單進程同時啟動 HTTP 與 workerMODE=server: 僅服務 API 與 proxy 層MODE=worker: 僅處理 queue 任務
若你直接用 CLI flag,會覆蓋 MODE 環境變數:
./bin/vylux --mode=server
./bin/vylux --mode=worker
單進程部署
當你的流量與任務量還小,最簡單的做法是直接跑 all:
docker run --rm \
--env-file .env \
-p 3000:3000 \
-v vylux-scratch:/var/cache/vylux \
ghcr.io/carry0987/vylux:latest
適合情境:
- 開發與 staging
- 單台 VM 或低流量環境
- 還不需要獨立水平擴展 worker
image 會固定設定 TMPDIR=/var/cache/vylux,並把 /var/cache/vylux 宣告成 Docker volume。若你不明確掛載,Docker 仍會幫你建立 anonymous volume;正式環境建議改成顯式 named volume 或平台管理磁碟,避免 scratch 空間使用量變得不透明。
Docker Compose 部署
repo 目前提供的 docker-compose.yml 會以 app service 啟動已發布的 ghcr.io/carry0987/vylux:latest 映像,預設跑 all mode,並同時啟動:
- PostgreSQL
- Redis
- 可選的 Cloudflare Tunnel
這個版本的特點:
apphealthcheck 走GET /healthz/var/cache/vylux掛為獨立 scratch volume- image 內的
TMPDIR也指向/var/cache/vylux,避免大型暫存資料散落在其他 temp 路徑 - 不再需要獨立的 key tmpfs,因為 raw encryption key 直接透過 Shaka Packager CLI 參數傳遞,不會先落成磁碟檔案
- repo 預設的 compose 檔並不會 publish host port
- 只有當你需要直接從 host 存取
http://localhost:3000時,才需要ports:映射
準備好 .env 後,最小啟動命令是:
docker compose up -d
預設 compose 檔是直接 pull 已發布的 GHCR image,因此不需要 --build。只有在你把 service 改成使用本機 build: 或其他自訂 image workflow 時,才需要加上 --build。
若你想直接從 host 存取服務,可以為 app service 加上 ports::
services:
app:
ports:
- "${PORT:-3000}:${PORT:-3000}"
ports: 是可省略的如果所有對外流量都只走 Cloudflare Tunnel,app service 就不需要 host ports: 映射。只有當你還想保留本機瀏覽器或 curl 直接測試時,才需要保留 ports:。
容器網路語意
在 docker-compose.yml 裡,每個容器都有自己的 localhost。也就是說:
app容器裡的localhost只會回到app自己tunnel容器裡的localhost只會回到cloudflared自己,不會連到app- 跨容器連線應使用 compose service name,例如
app、postgres、redis
因此,如果你讓 Vylux 本身跑在 compose 內,就不能直接沿用 host-only 範例中的 SOURCE_S3_ENDPOINT=http://localhost:9002,除非那個 storage endpoint 對該容器來說真的可達。
Cloudflare Tunnel sidecar
localhost 不是正確的 tunnel origin當 cloudflared 跑在 Docker 裡時,http://localhost:3000 指向的是 tunnel 容器自己,不是 app service。
tunnel sidecar 與 app 共用 compose network,因此 origin service 應設成 compose service name,例如 http://app:3000。
如果 cloudflared 跑在 Docker 裡,不要把 tunnel origin 指到 http://localhost:3000。這樣的 localhost 會解析到 tunnel 容器自己,通常會得到 502,並在日誌中看到 dial tcp [::1]:3000: connect: connection refused。
server / worker 拆分部署
正式環境更推薦把 server 與 worker 拆開,各自用同一個 image 啟動不同 args。
為什麼拆分
- API 與 worker 的 CPU / memory 壓力型態不同
- worker 需要獨立擴展處理 FFmpeg / libvips / packager 任務
- HTTP server 與 queue 消費可以分開做 probe、autoscaling 與故障隔離
Docker 範例
docker run -d \
--name vylux-server \
--env-file .env \
-p 3000:3000 \
ghcr.io/carry0987/vylux:latest \
--mode=server
docker run -d \
--name vylux-worker \
--env-file .env \
-p 3001:3001 \
-v vylux-scratch:/var/cache/vylux \
ghcr.io/carry0987/vylux:latest \
--mode=worker
若你的部署會處理大型轉碼,至少要一起考慮:
WORKER_CONCURRENCY:一般任務池併發度LARGE_WORKER_CONCURRENCY:video:large專用池併發度/var/cache/vylux的容量與 IO 性能
K8s 形態建議
K8s 下建議至少拆成兩個 Deployment:
vylux-servervylux-worker
兩者共用:
- 同一個 image
- 同一組 PostgreSQL、Redis、S3 credentials
- 同一組
API_KEY、WEBHOOK_SECRET、KEY_TOKEN_SECRET、ENCRYPTION_KEY
只需要在 Pod spec 覆蓋 args:
containers:
- name: vylux-server
image: ghcr.io/carry0987/vylux:latest
args: ["--mode=server"]
containers:
- name: vylux-worker
image: ghcr.io/carry0987/vylux:latest
args: ["--mode=worker"]
probe 建議
server:
- liveness:
GET /healthz - readiness:
GET /readyz - metrics:
GET /metrics
worker:
- liveness:
GET :WORKER_METRICS_PORT/healthz - metrics:
GET :WORKER_METRICS_PORT/metrics
若在 Kubernetes 上執行大型轉碼,每個 worker Pod 都應有足夠的 /var/cache/vylux 本地 scratch 容量。video:large 佇列被獨立出來,就是為了在限制大型任務併發的同時,不拖慢一般工作吞吐。
worker-only mode 沒有 /readyz HTTP endpoint,因此 readiness 通常可用啟動成功 + Redis / DB 外部依賴監控來補足,或在平台上以較保守的 startupProbe 控制。
儲存桶存取模型
- source store: 透過
SOURCE_S3_*對SOURCE_BUCKET做唯讀存取 - media store: 透過
MEDIA_S3_*對MEDIA_BUCKET做讀寫存取
這個模型讓上游應用與 Vylux 的權限邊界更清楚,也讓 server / worker 共享相同資料平面而無需本地持久卷。
部署檢查清單
DATABASE_URL、REDIS_URL、SOURCE_S3_*、MEDIA_S3_*、bucket 名稱已正確設定BASE_URL指向對外公開的媒體域名,且不帶 trailing slash- image 內或 runtime 內可找到
ffmpeg、vips、packager - server 與 worker 使用相同 secret material
- source bucket 與 media bucket 均可被 probe 與 runtime 存取
- metrics 與 tracing 已接到你的監控系統
不建議的做法
- 在 K8s 長期維持
allmode 再嘗試各自擴展 API 與 worker - 讓前端或公開客戶端持有
API_KEY、HMAC_SECRET、IMAGE_SOURCE_KEY或KEY_TOKEN_SECRET - 讓 source bucket 與 media bucket 混用相同寫入權限而失去權限邊界