B8 的 preset COPY 原本只加到 dev 的 Dockerfile.api-server,漏了 stage 實際用的 docker/Dockerfile.stage(multi-stage nginx runtime)→ stage image 無 preset 檔、/preset-models/* 下載 404。 runtime stage 補: - COPY --from=backend-builder /src/assets/preset-models → /app/assets/preset-models - ENV VISIONA_PRESET_MODELS_DIR=/app/assets/preset-models(baked 資產、隨 image 走) docker build --check 通過。Reviewer 複審通過。 注意:VISIONA_PRESET_BASE_URL + nginx /preset-models/ location 由 devops 在 stage 部署時補(Reviewer 指出的兩個 DevOps 配套缺口)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
165 lines
8.1 KiB
Docker
165 lines
8.1 KiB
Docker
# syntax=docker/dockerfile:1.6
|
||
#
|
||
# visionA — stage 部署映像(單 container 內含 nginx + node + 兩個 Go binary)
|
||
#
|
||
# 設計原則:
|
||
# - 仿 edge-ai-platform/docker/Dockerfile 的「nginx as front + 多 process 後端」模式
|
||
# - 但 visionA-frontend 採 Next.js `output: "standalone"`(不是 static export),
|
||
# 因此最終 image 必須帶 node runtime 跑 `node server.js`,不能像 edge-ai 那樣
|
||
# 只 COPY out/ 給 nginx serve。權衡見 docs/STAGE-DEPLOY.md §「為什麼帶 node」。
|
||
# - Multi-stage build:builder 帶完整 toolchain,runtime 只留 nginx + node-slim + 兩個 Go static binary
|
||
# - 最終 image 預估 ~150 MB(nginx-alpine ~50MB + node:lts-alpine ~120MB - overlap)
|
||
#
|
||
# Container 內 process(由 entrypoint.stage.sh 啟動):
|
||
# - nginx :80 公司 host nginx 反代到此 port
|
||
# - node server.js :3000 Next.js standalone server(內部)
|
||
# - api-server :3721 REST + storage
|
||
# - remote-proxy :3800/3801 tunnel WS + internal HTTP
|
||
#
|
||
# Build(在 visionA repo 根目錄):
|
||
# docker buildx build --platform linux/amd64 \
|
||
# -f docker/Dockerfile.stage \
|
||
# -t 192.168.0.130:5000/visiona:stage \
|
||
# --push .
|
||
#
|
||
# 本機驗證(不 push):
|
||
# docker buildx build --platform linux/amd64 \
|
||
# -f docker/Dockerfile.stage \
|
||
# -t test/visiona:stage --load .
|
||
|
||
# ============================================================
|
||
# Stage 1 — frontend builder
|
||
# ============================================================
|
||
# 用 Next.js 16 標準的 standalone build;產出 .next/standalone/ 與 .next/static/。
|
||
|
||
FROM node:22-alpine AS frontend-builder
|
||
|
||
# 啟用 corepack 以使用 repo 鎖定的 pnpm 版本。
|
||
# pnpm 9+ 對 lockfile v9 有要求;--frozen-lockfile 確保 CI 與本機一致。
|
||
RUN corepack enable
|
||
|
||
WORKDIR /src
|
||
|
||
# 先 COPY lockfile 讓依賴 layer 可以被 cache。
|
||
# pnpm-workspace.yaml 含 onlyBuiltDependencies trust list(pnpm 10 不執行依賴 build script,
|
||
# 必須明確 allow,否則乾淨環境第一次 install 撞 ERR_PNPM_IGNORED_BUILDS)。
|
||
COPY visionA-frontend/package.json visionA-frontend/pnpm-lock.yaml visionA-frontend/pnpm-workspace.yaml ./
|
||
RUN pnpm install --frozen-lockfile
|
||
|
||
# 複製其餘原始碼後 build。
|
||
COPY visionA-frontend/ ./
|
||
# Next.js 16 偵測到 output: "standalone" 後會輸出:
|
||
# .next/standalone/ → 含 server.js、必要 node_modules
|
||
# .next/static/ → 靜態 chunk(要單獨 COPY 進 standalone dir)
|
||
# public/ → 公開靜態資源(要單獨 COPY)
|
||
RUN pnpm build
|
||
|
||
# ============================================================
|
||
# Stage 2 — backend builder(api-server + remote-proxy)
|
||
# ============================================================
|
||
# CGO_ENABLED=0 → 純 Go static binary,alpine runtime 可直接執行。
|
||
|
||
FROM golang:1.26-alpine AS backend-builder
|
||
|
||
RUN apk add --no-cache git ca-certificates
|
||
|
||
WORKDIR /src
|
||
|
||
# 先 COPY go.mod / go.sum,讓依賴 layer 可被 cache。
|
||
COPY visionA-backend/go.mod visionA-backend/go.sum ./
|
||
RUN go mod download
|
||
|
||
COPY visionA-backend/ ./
|
||
|
||
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64
|
||
RUN go build -trimpath -ldflags="-s -w" -o /out/api-server ./cmd/api-server && \
|
||
go build -trimpath -ldflags="-s -w" -o /out/remote-proxy ./cmd/remote-proxy
|
||
|
||
# ============================================================
|
||
# Stage 3 — runtime(nginx + node + 兩個 binary)
|
||
# ============================================================
|
||
# 用 nginx:alpine 為基底,再裝 node + tini(PID 1 reaper)。
|
||
# tini 確保子 process 殘留 / signal forwarding 正確。
|
||
|
||
FROM nginx:alpine AS runtime
|
||
|
||
# nodejs — Next.js standalone server runtime(alpine 主 repo 當前版本,Next.js 16 支援 18-24)
|
||
# tini — 正確 PID 1,處理 signal + zombie reap(多 process container 必備)
|
||
# bash — entrypoint 用 wait -n,alpine 預設 sh 不支援
|
||
# curl — healthcheck 用
|
||
# ca-certificates / tzdata — 標準 hardening
|
||
#
|
||
# 注意:不固定 nodejs 版本(alpine repo 會隨時間升),如需固定改用 node:lts COPY 過來
|
||
RUN apk add --no-cache \
|
||
nodejs \
|
||
npm \
|
||
tini \
|
||
bash \
|
||
curl \
|
||
ca-certificates \
|
||
tzdata
|
||
|
||
# 建立非 root user(nginx 子 process 與 node 都用這個)
|
||
# nginx 主 process 必須是 root(要 bind :80)→ master 由 root 跑、worker fall back nginx user
|
||
RUN addgroup -S -g 1001 visiona && \
|
||
adduser -S -u 1001 -G visiona visiona
|
||
|
||
# ──────────── nginx 設定 ────────────
|
||
# 把預設站台清掉,換成 visionA 的 server block
|
||
RUN rm -f /etc/nginx/conf.d/default.conf
|
||
COPY docker/nginx.stage.conf /etc/nginx/conf.d/default.conf
|
||
|
||
# ──────────── frontend(Next.js standalone) ────────────
|
||
# Next.js 16 standalone 結構:
|
||
# /var/www/visiona/standalone/server.js
|
||
# /var/www/visiona/standalone/node_modules/ ← 必需
|
||
# /var/www/visiona/standalone/.next/ ← server 內部需要
|
||
# /var/www/visiona/static/ ← 對齊 server.js 預期:./。next/static
|
||
# /var/www/visiona/public/ ← 公開資源
|
||
#
|
||
# server.js 預設讀 ./.next/static 與 ./public 相對於自己的位置,
|
||
# 所以 standalone/ 下要再 mkdir .next/static 與 ./public 軟連結(或直接 COPY 進 standalone 內)。
|
||
WORKDIR /var/www/visiona
|
||
|
||
COPY --from=frontend-builder --chown=visiona:visiona /src/.next/standalone/ ./standalone/
|
||
COPY --from=frontend-builder --chown=visiona:visiona /src/.next/static/ ./standalone/.next/static/
|
||
COPY --from=frontend-builder --chown=visiona:visiona /src/public/ ./standalone/public/
|
||
|
||
# ──────────── backend binaries ────────────
|
||
COPY --from=backend-builder --chown=visiona:visiona /out/api-server /usr/local/bin/api-server
|
||
COPY --from=backend-builder --chown=visiona:visiona /out/remote-proxy /usr/local/bin/remote-proxy
|
||
RUN chmod +x /usr/local/bin/api-server /usr/local/bin/remote-proxy
|
||
|
||
# ──────────── 系統預設模型(B8) ────────────
|
||
# 7 個公用預設模型 .nef 打包進 image(GET /preset-models/* 直接串流、不簽 token)。
|
||
# 來源:backend-builder 的 /src/assets/preset-models(= repo 的 visionA-backend/assets/preset-models,
|
||
# 經 `COPY visionA-backend/ ./` 帶進 builder;.dockerignore 未排除 assets)。
|
||
# 與其他 backend 產物(api-server / remote-proxy)同樣自 backend-builder COPY,來源一致。
|
||
COPY --from=backend-builder --chown=visiona:visiona /src/assets/preset-models /app/assets/preset-models
|
||
|
||
# VISIONA_PRESET_MODELS_DIR 設成 image 內 COPY 目標的絕對路徑。
|
||
# 用 Dockerfile ENV(而非 .env.stage)的理由:preset 檔是 baked 進 image 的固定資產(不像
|
||
# /data/storage 是掛載 volume),路徑與 COPY 目標綁定、應隨 image 走,避免 .env.stage 漏設導致
|
||
# config/load.go 預設的相對路徑 `./assets/preset-models` 在 runtime workdir 讀不到而 404。
|
||
# 注意:VISIONA_PRESET_BASE_URL 不在此設(由 .env.stage 帶部署域名,devops 範圍)。
|
||
ENV VISIONA_PRESET_MODELS_DIR=/app/assets/preset-models
|
||
|
||
# ──────────── 共享資料目錄(local-fs storage 雛形用) ────────────
|
||
# api-server 預設 VISIONA_STORAGE_LOCALFS_ROOT=/data/storage
|
||
# 由 docker-compose volume 掛 host 路徑進來;image 預建 dir + chown 確保權限
|
||
RUN mkdir -p /data/storage && chown -R visiona:visiona /data
|
||
|
||
# ──────────── entrypoint ────────────
|
||
COPY docker/entrypoint.stage.sh /entrypoint.sh
|
||
RUN chmod +x /entrypoint.sh
|
||
|
||
# nginx 對外只 listen :80;由公司 host nginx 反代 :9527 → container :80。
|
||
EXPOSE 80
|
||
|
||
# Healthcheck — 打 nginx 的 /healthz(由 nginx.stage.conf 直接回 200,不打到 backend)
|
||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||
CMD curl -fsS http://127.0.0.1:80/healthz || exit 1
|
||
|
||
# tini 當 PID 1,把 signal + 子 process 收屍正確化
|
||
ENTRYPOINT ["/sbin/tini", "--", "/entrypoint.sh"]
|