# 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 # ──────────── 共享資料目錄(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"]