# Build & Deploy > 建置、本機開發、Docker 打包、部署的實務細節。 --- ## 1. Makefile(visionA-backend) ```makefile .PHONY: build build-api build-proxy build-dev test clean docker-build docker-up docker-down run-dev GO ?= go GO_FLAGS ?= -ldflags="-s -w" OUT_DIR ?= dist VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") build: build-api build-proxy build-api: $(GO) build $(GO_FLAGS) -o $(OUT_DIR)/api-server ./cmd/api-server build-proxy: $(GO) build $(GO_FLAGS) -o $(OUT_DIR)/remote-proxy ./cmd/remote-proxy test: $(GO) test -race -coverprofile=coverage.out ./... lint: $(GO) vet ./... gofmt -l . | grep -v '^$$' && exit 1 || true clean: rm -rf $(OUT_DIR) coverage.out # --- Docker --- docker-build: docker build -f docker/Dockerfile.api-server -t visiona/api-server:$(VERSION) . docker build -f docker/Dockerfile.remote-proxy -t visiona/remote-proxy:$(VERSION) . docker-up: docker compose -f docker/docker-compose.yml up --build docker-down: docker compose -f docker/docker-compose.yml down # --- Dev --- # 本機同時啟動兩個 binary(非雛形交付物,僅開發便利)。 # 交付物定義見 design-doc.md §2.4 Non-Goal。 run-dev: @trap 'kill 0' EXIT; \ $(GO) run ./cmd/remote-proxy & \ $(GO) run ./cmd/api-server & \ wait ``` --- ## 2. Dockerfile.api-server ```dockerfile # --- build stage --- FROM golang:1.26-alpine AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/api-server ./cmd/api-server # --- runtime stage --- FROM gcr.io/distroless/static:nonroot WORKDIR /app COPY --from=build /out/api-server /app/api-server USER nonroot:nonroot EXPOSE 3001 ENTRYPOINT ["/app/api-server"] ``` ## 3. Dockerfile.remote-proxy ```dockerfile FROM golang:1.26-alpine AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/remote-proxy ./cmd/remote-proxy FROM gcr.io/distroless/static:nonroot WORKDIR /app COPY --from=build /out/remote-proxy /app/remote-proxy USER nonroot:nonroot EXPOSE 3800 3801 ENTRYPOINT ["/app/remote-proxy"] ``` --- ## 4. docker/docker-compose.yml ```yaml services: api-server: build: context: ../ dockerfile: docker/Dockerfile.api-server image: visiona/api-server:dev ports: ["3001:3001"] environment: VISIONA_API_PORT: "3001" VISIONA_SESSION_BACKEND: inmemory VISIONA_STORAGE_BACKEND: localfs VISIONA_STORAGE_LOCALFS_ROOT: /data/storage VISIONA_STORAGE_LOCALFS_BASE_URL: http://localhost:3001/storage VISIONA_STORAGE_SIGNING_SECRET: dev-secret-do-not-use-in-prod VISIONA_AUTH_MODE: static VISIONA_STATIC_USER_ID: demo-user VISIONA_PAIRING_MODE: static VISIONA_PAIRING_TOKEN: "${VISIONA_PAIRING_TOKEN}" VISIONA_CONVERTER_MODE: stub volumes: - storage-data:/data remote-proxy: build: context: ../ dockerfile: docker/Dockerfile.remote-proxy image: visiona/remote-proxy:dev ports: - "3800:3800" # tunnel - "3801:3801" # internal environment: VISIONA_TUNNEL_PORT: "3800" VISIONA_PROXY_INTERNAL_PORT: "3801" VISIONA_SESSION_BACKEND: inmemory VISIONA_PAIRING_MODE: static VISIONA_PAIRING_TOKEN: "${VISIONA_PAIRING_TOKEN}" # 雛形設計(ADR-006 / Q1): # - remote-proxy 是唯一持有 yamux.Session 的 process(in-memory) # - api-server 無狀態,透過 internal HTTP 向 remote-proxy 查詢 session # - 兩 binary 之間用 VISIONA_PROXY_INTERNAL_URL 連結(下方 api-server 已設 env) volumes: storage-data: ``` 另外 api-server service 需要新增 env(指向 remote-proxy 的 internal URL): ```yaml api-server: environment: # 上方所有 env 保留,新增: VISIONA_SESSION_BACKEND: proxy-client VISIONA_PROXY_INTERNAL_URL: http://remote-proxy:3801 ``` ### 4.1 雛形推薦的開發方式 ```bash # .env VISIONA_PAIRING_TOKEN="vAc_$(openssl rand -hex 16)" # 格式見 security.md §1.3 # 方式 1:本機 Makefile 平行跑兩個 binary(開發便利工具) make run-dev # 方式 2:Docker Compose(更接近 Production 拓撲) make docker-up ``` **結論**:雛形交付物是雙 binary + docker-compose(兩者皆可用於 demo);`make run-dev` 僅為本機開發便利工具(**非交付物**,見 design-doc.md §2.4 Non-Goal)。 --- ## 5. 前端建置 ```bash cd visionA-frontend pnpm install pnpm dev # 本機開發 http://localhost:3000 pnpm build # 產出 .next/ pnpm start # 生產模式跑 Next.js server ``` ### 5.1 Next.js Build 模式 | 模式 | 用途 | 如何設定 | |------|------|---------| | `next build` + `next start` | SSR / ISR 支援 | 適合 Phase 1,需要 Next.js runtime | | `output: 'export'` | 靜態 export,放 CDN | 若所有頁面都可靜態化,最便宜 | **雛形建議**:`next start` on Node,方便 API rewrites / middleware;Phase 1 視需求切換。 ### 5.2 環境變數 ``` # visionA-frontend/.env.example NEXT_PUBLIC_API_BASE=http://localhost:3001 NEXT_PUBLIC_WS_BASE=ws://localhost:3001 # 雛形開發用: NEXT_PUBLIC_DEV_PAIRING_TOKEN= ``` --- ## 6. 本機完整開發流程 ```bash # Terminal 1 — Backend cd visionA-backend export VISIONA_PAIRING_TOKEN=$(openssl rand -hex 32) export VISIONA_STATIC_USER_ID=demo-user make run-dev # api at :3001, tunnel at :3800 # Terminal 2 — local agent (local-tool 現有 + 開雲端模式,或 POC 的 edge-ai-server) cd /Users/jimchen/Innovedus/edge-ai-platform/edge-ai-platform ./dist/edge-ai-server --relay-url=ws://localhost:3800/tunnel/connect --relay-token=$VISIONA_PAIRING_TOKEN # Terminal 3 — Frontend cd visionA-frontend cp .env.example .env.local # 編輯 .env.local 填入 PAIRING_TOKEN pnpm dev # http://localhost:3000 ``` --- ## 7. Phase 1 部署草圖 ### 7.1 AWS ECS Fargate + Application Load Balancer ``` ┌──────────────────────────────────────────────────────────────┐ │ Route 53 — api.visiona.cloud / proxy.visiona.cloud │ └─────────────────────┬────────────────────┬───────────────────┘ │ │ ▼ ▼ ┌─────────────────┐ ┌───────────────────┐ │ ALB │ │ NLB │ │ (HTTPS/WSS) │ │ (TCP passthrough)│ │ api.* │ │ proxy.* │ └────────┬────────┘ └─────────┬─────────┘ │ │ ▼ ▼ ┌─────────────────────┐ ┌─────────────────────┐ │ ECS Service: │ │ ECS Service: │ │ api-server │ │ remote-proxy │ │ (Fargate, 2+ tasks)│ │ (Fargate, 2+ tasks) │ └──────┬──────────────┘ └──────────┬──────────┘ │ │ └──────┬───────────────┬──────┘ │ │ ▼ ▼ ┌──────────────┐ ┌─────────────────┐ │ ElastiCache │ │ RDS │ │ Redis │ │ PostgreSQL │ └──────────────┘ └─────────────────┘ ▲ ▲ │ │ └────────────────┘ │ ┌──────────────┐ │ S3 bucket │ └──────────────┘ ``` ### 7.2 Kubernetes 方案 - api-server:`Deployment` + `HorizontalPodAutoscaler` - remote-proxy:`Deployment` + HPA(按 tunnel 數 metric) - Redis / Postgres:managed service 或 StatefulSet - Ingress:nginx-ingress 或 cloud LB controller **Cloud-agnostic 原則**:Helm chart 不綁特定雲,storage / DB 依 env 注入連線資訊。 --- ## 8. CI/CD(Phase 1 規劃) ### 8.1 GitHub Actions ```yaml # .github/workflows/ci.yml name: CI on: [push, pull_request] jobs: backend-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: { go-version: '1.26' } - run: cd visionA-backend && make test - run: cd visionA-backend && make lint frontend-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v3 - uses: actions/setup-node@v4 with: { node-version: '20', cache: 'pnpm' } - run: cd visionA-frontend && pnpm install --frozen-lockfile - run: cd visionA-frontend && pnpm test - run: cd visionA-frontend && pnpm build ``` ### 8.2 Release Pipeline(Phase 1) 1. PR merged to `main` 2. CI 跑 test + lint + build 3. 產出 Docker image → push to registry(ECR / GCR / GitHub Packages) 4. Tag image 為 `main-` 5. 手動觸發 deploy(或 GitOps 自動)→ ECS task definition 更新 / K8s rollout --- ## 9. 環境變數對照表(摘要) | Env | 雛形 | Phase 1 | |-----|------|--------| | `VISIONA_AUTH_MODE` | `static` | `clerk` / `oidc` | | `VISIONA_PAIRING_MODE` | `static` | `db` | | `VISIONA_SESSION_BACKEND` | `inmemory` | `redis` | | `VISIONA_STORAGE_BACKEND` | `localfs` | `s3` | | `VISIONA_CONVERTER_MODE` | `stub` | `http` | | `VISIONA_REDIS_URL` | — | `redis://...` | | `VISIONA_DB_URL` | — | `postgres://...` | | `VISIONA_S3_*` | — | 實際 credentials | --- **雛形實作重點**: - `make run-dev`(單 binary 兩 listener) - 不需要 Docker;Docker 檔案寫好以備 Phase 1 - 不需要 CI;但 Makefile 有 `test` + `lint` 方便本機檢查 **Phase 1 必做**: - Docker image CI 產出 - K8s / ECS manifest - Blue-green 或 rolling deploy - Staging 環境