新增雲端版部署設定(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>
149 lines
6.7 KiB
Docker
149 lines
6.7 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。
|
||
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 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"]
|