v1 (deploy-stage.sh) 走 docker save | gzip | docker load 模式,需要把 81MB tarball 一次性 POST 到 stage docker daemon /images/load API。5/4 / 5/9 兩次 驗證 VPN 下 docker daemon 對單一大 POST hang(卡 30+ 分鐘 / i/o timeout), 公司網段直連才可靠。 v2 仿 edge-ai-platform/scripts/deploy-docker.sh 改用 DOCKER_HOST=stage docker build — multi-stage build 完全在 stage daemon 上執行: - 跨網路只傳 build context(~44 MB streaming,VPN 友善) - alpine base / nodejs / go mod / pnpm install 都由 stage daemon 自己 pull - layer cache 留在 stage daemon,後續 incremental build 更快 - 5/9 VPN 下實測 work:first build ~3min、redeploy(layer cache) ~10s 連帶修: - pnpm-workspace.yaml: 加 onlyBuiltDependencies (sharp / unrs-resolver / @tailwindcss/oxide / esbuild) — pnpm 10 預設拒跑依賴 build script、 乾淨環境第一次 install 撞 ERR_PNPM_IGNORED_BUILDS - package.json: 加 packageManager: pnpm@10.30.1 — 鎖 pnpm 版本,corepack 在 stage daemon 第一次跑時不會拉到最新 pnpm 11(行為差異) - Dockerfile.stage: COPY pnpm-workspace.yaml 進 builder context、否則 容器內 install 看不到 trust list v1 (deploy-stage.sh) 保留作為公司網段直連備援;v2 是 VPN / 預設模式。
151 lines
6.9 KiB
Docker
151 lines
6.9 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
|
||
|
||
# ──────────── 共享資料目錄(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"]
|