visionA/docker/Dockerfile.stage
jim800121chen eb66a7287a feat(deploy): visionA Cloud dev / stage docker compose + Caddy/nginx + 部署腳本
新增雲端版部署設定(Phase 0.6 dev + Phase 0.7 stage 分兩套):

dev 環境(docker-compose.dev.yml):
- 5 service all-in-one(postgres + member-center + visionA-backend + frontend + Caddy)
- Caddy 自動 HTTPS for localhost
- .env.dev.example 範本(使用者拷出 .env.dev 後 docker compose up -d)
- Makefile dev-with-mc 9 個 target

stage 環境(docker-compose.stage.yml + docker/Dockerfile.stage):
- multi-stage build(node22 frontend + go1.26 backend × 2 + nginx-alpine runtime)
  最終 image 319 MB,含 nginx + nodejs + tini + bash
- entrypoint.stage.sh 4 process 共命運(nginx + api-server + remote-proxy +
  next.js standalone)用 wait -n + SIGTERM trap
- nginx.stage.conf:白名單 server_name stage-9527.innovedus.com + 444 default_server
  + /healthz 例外(127.0.0.0/8 only)+ /api/ 與 /storage/ 強制 no-store
  + /tunnel/connect WS upgrade + 100M body / 3600s timeout
- 對外 mapping 0.0.0.0:9527:80(公司 host nginx 在外層處理 HTTPS termination
  — Let's Encrypt stage-9527.innovedus.com 自動續簽)
- named volume visiona-data(不用 bind mount,因 stage docker daemon 在 host root
  無 mkdir 權限)

部署腳本(scripts/deploy-stage.sh):
- 仿 edge-ai-platform/scripts/deploy-docker.sh 早期 save/load 模式
- 為什麼不用 internal registry:公司 192.168.0.130:5000 開了 auth、無帳密
- 流程:buildx --load → docker save | gzip → DOCKER_HOST docker load → compose up
- 含 --rollback <tag> / --skip-build / --no-push / --skip-deploy 選項
- timestamp + git SHA tag 留 rollback 餘地

文件(docs/):
- DEV-SETUP.md:dev 環境一鍵起步驟
- SMOKE-TEST.md:手動煙測 checklist(OIDC flow / pairing / tunnel)
- STAGE-DEPLOY.md:stage 完整手冊(架構圖 / 環境前置 / 部署 step / rollback /
  7 種故障排除 / 緊急救回 POC)

.env.stage.example 對齊 backend A1 改造:
- VISIONA_OIDC_CLIENT_SECRET 留空(PKCE-only public client)
- VISIONA_OIDC_SERVICE_CLIENT_ID/_SECRET 留空(Phase 1 預留鉤子)
- 所有 secret 用 placeholder(CHANGE_ME_OPENSSL_RAND_HEX_32)

.dockerignore:避免 node_modules / .next / .git 等進 build context

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:22:44 +08:00

149 lines
6.7 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。
COPY visionA-frontend/package.json visionA-frontend/pnpm-lock.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
# ──────────── 共享資料目錄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"]