visionA/docker/Dockerfile.stage
jim800121chen 1d4977a974 fix(stage): Dockerfile.stage 補 preset 模型 COPY(B8 stage 修正)
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>
2026-06-28 07:46:43 +08:00

165 lines
8.1 KiB
Docker
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 buildbuilder 帶完整 toolchainruntime 只留 nginx + node-slim + 兩個 Go static binary
# - 最終 image 預估 ~150 MBnginx-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 listpnpm 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 builderapi-server + remote-proxy
# ============================================================
# CGO_ENABLED=0 → 純 Go static binaryalpine 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 — runtimenginx + node + 兩個 binary
# ============================================================
# 用 nginx:alpine 為基底,再裝 node + tiniPID 1 reaper
# tini 確保子 process 殘留 / signal forwarding 正確。
FROM nginx:alpine AS runtime
# nodejs — Next.js standalone server runtimealpine 主 repo 當前版本Next.js 16 支援 18-24
# tini — 正確 PID 1處理 signal + zombie reap多 process container 必備)
# bash — entrypoint 用 wait -nalpine 預設 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 usernginx 子 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
# ──────────── frontendNext.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 打包進 imageGET /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"]