diff --git a/.dockerignore b/.dockerignore index 96abb60..4ad0b73 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,7 +14,6 @@ tmp-cookie.txt dist/ frontend/node_modules/ frontend/dist/ -embed_static/static/spa/ node_modules/ .vite/ diff --git a/Dockerfile b/Dockerfile index 61cedfc..ad088b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,20 @@ -# 姜十三论坛 — 多阶段构建:Node 前端 → Go 单二进制 → Alpine 运行镜像 +# 姜十三论坛 — 多阶段构建:Node 前端(Vike SSR)+ Go API,双服务共用镜像 # 不使用 # syntax=docker/dockerfile:1,避免构建前额外拉取 docker.io/docker/dockerfile # # 国内网络:默认经 DaoCloud 拉取基础镜像,npm/go 走国内代理 # 海外或已配置 Docker registry-mirrors 时可传空前缀: # docker build --build-arg IMAGE_PREFIX= ... +# +# 运行形态(docker compose 双容器,同一镜像两种角色): +# api —— Go 二进制,纯 API(:3001 内部,/data 数据卷) +# web —— Node 渲染服务器(对外 :3000:SSR + 静态资源 + API 反代) +# 镜像默认入口为 api 角色;web 角色在 compose 中以 node 用户覆盖 entrypoint。 ARG IMAGE_PREFIX=docker.m.daocloud.io/library/ ARG VERSION=dev -# ── Stage 1: 前端构建(Vite → embed_static/static/spa)──────────────────── -FROM ${IMAGE_PREFIX}node:22-bookworm-slim AS frontend +# ── Stage 1: 前端构建(Vike → dist/client 静态资源 + dist/server SSR 入口)── +FROM ${IMAGE_PREFIX}node:24-bookworm-slim AS frontend WORKDIR /src/frontend COPY frontend/package.json frontend/package-lock.json ./ RUN npm config set registry https://registry.npmmirror.com \ @@ -17,7 +22,7 @@ RUN npm config set registry https://registry.npmmirror.com \ COPY frontend/ ./ RUN npm run build -# ── Stage 2: Go 编译(纯 Go SQLite,CGO_ENABLED=0)──────────────────────── +# ── Stage 2: Go 编译(纯 Go SQLite,CGO_ENABLED=0;仅 API,无内嵌前端)───── FROM ${IMAGE_PREFIX}golang:1.26-bookworm AS builder ARG VERSION ENV GOPROXY=https://goproxy.cn,direct @@ -25,31 +30,31 @@ WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . -COPY --from=frontend /src/embed_static/static/spa ./embed_static/static/spa RUN CGO_ENABLED=0 go build -trimpath \ -ldflags "-s -w -X main.version=${VERSION}" \ -o /out/jiang13 ./cmd/jiang13 -# ── Stage 3: 运行镜像 ─────────────────────────────────────────────────────── -FROM ${IMAGE_PREFIX}alpine:3.21 -RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ - && apk add --no-cache ca-certificates tzdata wget su-exec \ - && adduser -D -u 1000 -h /app jiang13 +# ── Stage 3: 运行镜像(Node 运行时承载 SSR 渲染与静态资源,附带 Go API 二进制) +FROM ${IMAGE_PREFIX}node:24-bookworm-slim +RUN apt-get update \ + && apt-get install -y --no-install-recommends gosu ca-certificates tzdata \ + && rm -rf /var/lib/apt/lists/* -ENV TZ=Asia/Shanghai +ENV TZ=Asia/Shanghai \ + NODE_ENV=production WORKDIR /app COPY --from=builder /out/jiang13 /app/jiang13 +COPY --from=frontend /src/frontend /app/frontend COPY docker-entrypoint.sh /docker-entrypoint.sh -RUN chown jiang13:jiang13 /app/jiang13 \ - && chmod +x /app/jiang13 /docker-entrypoint.sh +RUN chmod +x /app/jiang13 /docker-entrypoint.sh -# 以 root 启动,entrypoint 修正 /data 卷权限后 su-exec 降权为 jiang13 +# 以 root 启动,entrypoint 修正 /data 卷权限后 gosu 降权为 uid 1000(node 用户) VOLUME ["/data"] -EXPOSE 3000 +EXPOSE 3000 3001 HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ - CMD wget -qO- http://127.0.0.1:3000/health || exit 1 + CMD node -e "fetch('http://127.0.0.1:3001/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["--work-path", "/app", "--data", "/data", "--port", "3000"] +CMD ["--work-path", "/app", "--data", "/data", "--port", "3001"] diff --git a/Makefile b/Makefile index a3f39ee..d573417 100644 --- a/Makefile +++ b/Makefile @@ -12,39 +12,40 @@ REGISTRY_IMAGE := hangzhang714128/jiang13-forum GO := go GOFLAGS := -trimpath -.PHONY: all build build-windows build-linux build-darwin clean run dev tidy help frontend frontend-build docker compose-up compose-down +.PHONY: all build web-build build-windows build-linux build-darwin build-all clean run serve dev tidy help docker compose-up compose-down all: build -frontend-build: - cd frontend && npm install && npm run build +## 前端构建(Vike:产出 frontend/dist/client 静态资源 + dist/server SSR 入口) +web-build: + cd frontend && (test -d node_modules || npm install) && npm run build -## 编译当前平台二进制(纯 Go SQLite,无需 CGO) -build: frontend-build +## 编译当前平台二进制(纯 Go SQLite,无需 CGO;Go 仅含 API,前端由 Node 渲染) +build: @mkdir -p $(BUILD_DIR) CGO_ENABLED=0 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PKG) @echo "✓ 编译完成: $(BUILD_DIR)/$(APP_NAME)" -## Windows amd64(先打包前端再 embed) -build-windows: frontend-build +## Windows amd64 +build-windows: @mkdir -p $(BUILD_DIR) CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME).exe $(MAIN_PKG) @echo "✓ Windows: $(BUILD_DIR)/$(APP_NAME).exe" -## Linux amd64(先打包前端再 embed) -build-linux: frontend-build +## Linux amd64 +build-linux: @mkdir -p $(BUILD_DIR) CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PKG) @echo "✓ Linux: $(BUILD_DIR)/$(APP_NAME)-linux-amd64" -## macOS arm64 (Apple Silicon)(先打包前端再 embed) -build-darwin: frontend-build +## macOS arm64 (Apple Silicon) +build-darwin: @mkdir -p $(BUILD_DIR) CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME)-darwin-arm64 $(MAIN_PKG) @echo "✓ macOS: $(BUILD_DIR)/$(APP_NAME)-darwin-arm64" -## 跨平台全量编译(frontend-build 只跑一次) -build-all: frontend-build +## 跨平台全量编译 +build-all: @mkdir -p $(BUILD_DIR) CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME).exe $(MAIN_PKG) CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PKG) @@ -56,12 +57,19 @@ build-all: frontend-build tidy: $(GO) mod tidy -## 本地运行(仅后端,使用已 embed 的前端;数据目录与 dist 二进制一致) +## 本地运行(仅 Go API :3000,无前端页面;数据目录与 dist 二进制一致) run: @mkdir -p $(DEV_DATA_DIR) $(GO) run $(MAIN_PKG) --data $(DEV_DATA_DIR) -## 前端热更新开发(后端 :3000 + Vite :5173,Ctrl+C 同时退出;数据目录与 dist 二进制一致) +## 生产模式本地运行(Node :3000 对外 SSR + Go API :3001) +serve: build web-build + @mkdir -p $(DEV_DATA_DIR) + @trap 'kill 0' INT; \ + $(BUILD_DIR)/$(APP_NAME) --data $(DEV_DATA_DIR) --port 3001 & \ + cd frontend && PORT=3000 GO_API_URL=http://127.0.0.1:3001 npm start + +## 前端热更新开发(Vike dev :5173 + Go API :3000,Ctrl+C 同时退出;数据目录与 dist 二进制一致) dev: @echo "前端热更新: http://localhost:5173" @echo "后端 API : http://localhost:3000" @@ -79,7 +87,7 @@ clean: docker: docker build --build-arg VERSION=$(VERSION) -t $(REGISTRY_IMAGE):$(VERSION) -t $(REGISTRY_IMAGE):latest . -## Docker Compose 启动 +## Docker Compose 启动(web :3000 + api :3001 双容器) compose-up: docker compose up -d --build @@ -89,12 +97,14 @@ compose-down: help: @echo "姜十三论坛编译命令:" - @echo " make build - 编译当前平台" + @echo " make build - 编译 Go API 二进制(当前平台)" + @echo " make web-build - 构建前端(Vike:client + server 产物)" + @echo " make serve - 生产模式本地运行(Node :3000 + Go API :3001)" @echo " make build-windows - 编译 Windows" @echo " make build-linux - 编译 Linux" @echo " make build-darwin - 编译 macOS" @echo " make build-all - 编译全部平台" - @echo " make run - 仅启动后端(:3000)" + @echo " make run - 仅启动 Go API(:3000)" @echo " make dev - 前端热更新开发(:5173 + :3000)" @echo " make docker - 构建 Docker 镜像" @echo " make compose-up - Docker Compose 启动" diff --git a/README.md b/README.md index bbb69f3..e7310b9 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-18a058?style=flat-square)](LICENSE) [![Docker](https://img.shields.io/badge/Docker-hangzhang714128%2Fjiang13--forum-2496ED?style=flat-square&logo=docker&logoColor=white)](https://hub.docker.com/r/hangzhang714128/jiang13-forum) [![Go](https://img.shields.io/badge/Go-1.26-00ADD8?style=flat-square&logo=go&logoColor=white)](go.mod) -[![React](https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react&logoColor=white)](frontend/package.json) +[![React](https://img.shields.io/badge/React-19-61DAFB?style=flat-square&logo=react&logoColor=white)](frontend/package.json) [![SQLite](https://img.shields.io/badge/SQLite-内置-003B57?style=flat-square&logo=sqlite&logoColor=white)](#) [在线演示](https://bbs.iioio.com/) · @@ -114,11 +114,12 @@ ### 部署体验 -- **单二进制** — `go:embed` 打包前端,无需再单独部署静态资源 +- **双服务架构** — Node(:3000 对外:SSR + 静态资源 + API 反代)+ Go(:3001 内部:纯 API),职责单一、可独立扩缩 +- **服务端渲染** — 首页 / 板块 / 帖子详情由 Vike SSR 真渲染,SEO 友好、首屏直出;其余页面 SPA 接管 - **零依赖数据库** — SQLite 内建,数据目录由 `app.ini` 统一管理 -- **跨平台** — Windows / Linux / macOS 一键编译 +- **跨平台** — Windows / Linux / macOS 一键编译 Go API - **系统服务** — 内置 Linux systemd / Windows Service 注册 -- **Docker 单容器** — 多阶段镜像,挂载 `data/` 即可持久化 +- **Docker 双容器** — 同一镜像两种角色,`docker compose up` 一键起 --- @@ -126,7 +127,7 @@ ### 1. 编译 -**Windows(推荐):** +Go API 二进制(不含前端,Windows): ```bat build.bat @@ -142,9 +143,21 @@ make build **手动分步(全平台):** +```bash +go build -trimpath -ldflags "-s -w" -o dist/jiang13 ./cmd/jiang13 +``` + +前端构建(Vike,产出 `frontend/dist/client` + `dist/server`): + ```bash cd frontend && npm install && npm run build -cd .. && go build -trimpath -ldflags "-s -w" -o dist/jiang13 ./cmd/jiang13 +``` + +本地生产模式一键起双服务(Node :3000 对外 + Go API :3001): + +```bash +make serve # Linux / macOS +build.bat -Target serve # Windows ``` 跨平台编译: @@ -180,31 +193,38 @@ make compose-up **拉取已构建镜像(Docker Hub):** +同一镜像两种角色,建议直接用 `docker-compose.yml`(已含完整配置): + ```bash docker pull hangzhang714128/jiang13-forum:latest -docker run -d --name jiang13 \ - -p 3000:3000 \ - -v jiang13-data:/data \ - --restart unless-stopped \ - hangzhang714128/jiang13-forum:latest +docker compose up -d ``` -**数据持久化:** 容器内 `/data` 对应 SQLite、上传、日志与 JWT 密钥,与下方「数据目录」结构一致。可用 Docker volume 或绑定宿主机目录。镜像启动时会自动将 `/data` 卷属主修正为 uid `1000`(`jiang13` 用户),适配 1Panel 等面板挂载的目录。 +Compose 会启动两个容器: + +| 容器 | 角色 | 端口 | 说明 | +|------|------|------|------| +| `web` | Node 渲染服务器 | `3000` 对外 | Vike SSR + 静态资源 + API 反代(`GO_API_URL=http://api:3001`) | +| `api` | Go API | `3001` 内部 | 纯 API 服务,持有 `/data` 数据卷 | + +**数据持久化:** `api` 容器内 `/data` 对应 SQLite、上传、日志与 JWT 密钥,与下方「数据目录」结构一致。可用 Docker volume 或绑定宿主机目录。镜像启动时会自动将 `/data` 卷属主修正为 uid `1000`,适配 1Panel 等面板挂载的目录。 **若使用旧版镜像仍报 permission denied**,可在宿主机执行:`chown -R 1000:1000 /你的数据目录` **可选环境变量(容器编排):** -| 变量 | 说明 | -|------|------| -| `JIANG13_HTTP_PORT` | HTTP 端口(默认 `3000`) | -| `JIANG13_DATA` | 数据目录(默认 `/data`) | -| `JIANG13_JWT_SECRET` | JWT 密钥(留空则自动生成并写入 `/data/.jwt_secret`) | -| `JIANG13_CONFIG` | 配置文件路径 | -| `JIANG13_WORK_PATH` | 工作目录 | -| `JIANG13_COMMUNITY_HUB` | 维护者选项:设为 `1` 时本站作为社区枢纽收报。官方站 `bbs.iioio.com` 会按域名自动成为枢纽,无需此变量;仅非官网的枢纽镜像 / 预发需要显式开启(见 `app.ini.example`) | +| 变量 | 容器 | 说明 | +|------|------|------| +| `GO_API_URL` | web | Go API 地址(默认 `http://127.0.0.1:3001`,Compose 中为 `http://api:3001`) | +| `PORT` | web | Node 渲染服务器监听端口(默认 `3000`) | +| `JIANG13_HTTP_PORT` | api | HTTP 端口(默认 `3000`,Compose 中为 `3001`) | +| `JIANG13_DATA` | api | 数据目录(默认 `/data`) | +| `JIANG13_JWT_SECRET` | api | JWT 密钥(留空则自动生成并写入 `/data/.jwt_secret`) | +| `JIANG13_CONFIG` | api | 配置文件路径 | +| `JIANG13_WORK_PATH` | api | 工作目录 | +| `JIANG13_COMMUNITY_HUB` | api | 维护者选项:设为 `1` 时本站作为社区枢纽收报。官方站 `bbs.iioio.com` 会按域名自动成为枢纽,无需此变量;仅非官网的枢纽镜像 / 预发需要显式开启(见 `app.ini.example`) | -**健康检查:** `GET /health` 返回 `{"status":"ok"}`,供 Docker / 负载均衡探活。 +**健康检查:** Go API `GET /health`、Node 渲染服务器 `GET /healthz`,均返回 200,供 Docker / 负载均衡探活。 **发布镜像到 Docker Hub(手动):** @@ -238,14 +258,16 @@ docker push hangzhang714128/jiang13-forum:latest **1Panel 部署提示:** -1. 容器镜像填 `hangzhang714128/jiang13-forum:latest` -2. 端口映射 `3000:3000` -3. 挂载数据卷到容器内 `/data`(镜像会自动修正目录权限) +1. 用 Compose 编排(`docker-compose.yml`)或手动创建两个容器:`api`(Go,内部 `3001`,挂载 `/data`)与 `web`(Node,映射 `3000:3000`) +2. `web` 容器环境变量 `GO_API_URL` 指向 `api` 容器(如 `http://api:3001`) +3. 数据卷只需挂在 `api` 容器的 `/data`(镜像会自动修正目录权限) 4. 首次访问 `http://服务器IP:3000/register` 注册管理员 ### 3. 直接启动(二进制) -不想自己编译时,从 [Gitea Releases](https://git.iioio.com/freefire/jiang13-forum/releases) 下载与 Docker 同版本号的文件即可: +Go 二进制仅提供 API,完整站点需同时运行 Node 渲染服务器(构建前端后 `frontend/server/index.mjs`)。最简单的方式是用上面的 Docker Compose,或本地 `make serve` / `build.bat -Target serve` 一键起双进程。 + +如需手动部署二进制,从 [Gitea Releases](https://git.iioio.com/freefire/jiang13-forum/releases) 下载与 Docker 同版本号的文件: | 文件 | 平台 | |------|------| @@ -254,35 +276,24 @@ docker push hangzhang714128/jiang13-forum:latest 当前 **1.1.7:** [Windows](https://git.iioio.com/freefire/jiang13-forum/releases/download/v1.1.7/jiang13-1.1.7-windows-amd64.exe) · [Linux](https://git.iioio.com/freefire/jiang13-forum/releases/download/v1.1.7/jiang13-1.1.7-linux-amd64) -把文件放到目标目录后直接运行(首次会在同目录生成 `app.ini`): +两个进程一起跑(Go API :3001 + Node :3000 对外): ```bash -# Windows -.\jiang13-1.1.7-windows-amd64.exe -# 或改名为 jiang13.exe 后: -.\jiang13.exe +# 1) 启动 Go API +./jiang13 --port 3001 --data ./data -# Linux -chmod +x jiang13-1.1.7-linux-amd64 -./jiang13-1.1.7-linux-amd64 +# 2) 另开终端,构建并启动 Node 渲染服务器 +cd frontend && npm install && npm run build +PORT=3000 GO_API_URL=http://127.0.0.1:3001 npm start ``` -本地刚编译的产物在 `dist/`: +Windows: -```bash -# Windows -.\dist\jiang13.exe - -# Linux / macOS -./dist/jiang13 -``` - -也可先复制示例配置再改端口 / 数据目录: - -```bash -cp app.ini.example /opt/jiang13/app.ini -# 编辑 app.ini 后: -./jiang13 +```powershell +.\jiang13.exe --port 3001 --data .\data +# 另开 PowerShell: +cd frontend; npm install; npm run build +$env:PORT = '3000'; $env:GO_API_URL = 'http://127.0.0.1:3001'; npm start ``` ### 4. 首次使用 @@ -354,16 +365,17 @@ C:\jiang13\jiang13.exe --service start | 层级 | 技术 | |------|------| -| **后端** | Go 1.26 · Gin · GORM · SQLite | -| **前端** | React 18 · TipTap · Radix UI · Tailwind CSS · TanStack Virtual | -| **构建** | Vite → `go:embed` 内嵌 SPA,单二进制发布 | +| **后端** | Go 1.26 · Gin · GORM · SQLite(纯 API 服务) | +| **前端** | React 19 · Vike SSR · react-router · TipTap · Radix UI · Tailwind CSS · TanStack Virtual | +| **构建** | Vite 8(Rolldown)双构建:`dist/client` 静态资源 + `dist/server` SSR 入口 | +| **渲染** | Node 24 · Express + Vike `renderPage`(:3000 对外,API 反代至 Go :3001) | | **认证** | bcrypt · JWT Cookie · 可选 OIDC Provider | --- ## 前端开发 -日常改前端不需要重新完整构建,Vite 支持秒级热更新(HMR,热模块替换): +日常改前端不需要重新完整构建,Vike dev 支持秒级热更新(HMR,热模块替换): ```bat build.bat -Target dev @@ -373,16 +385,17 @@ build.bat -Target dev make dev ``` -浏览器访问 `http://localhost:5173`,API 自动代理到 `http://localhost:3000`。 +浏览器访问 `http://localhost:5173`,API 自动代理到 `http://localhost:3000`。首页 / 板块 / 帖子详情在 dev 下同样走 SSR(`vike dev`),与生产行为一致;其余页面为 SPA 壳。 开发后端与 `dist/jiang13` 共用数据目录 `dist/data`(SQLite、上传、JWT 密钥等),避免 dev 与 dist 运行数据不一致。 **何时需要完整构建:** -- 修改 Go 代码或要发布单二进制 → `build.bat` / `make build` +- 修改 Go 代码 → `build.bat` / `make build`(仅编译 Go API) +- 验证生产 SSR / 发布部署 → `make serve`(构建前端 + 双进程起本地产) - 更新 README 界面截图 → 见下方「更新截图」 -> 直接访问 `:3000` 看到的是上次 build 嵌入的前端;开发时请用 `:5173`。 +> 开发时请访问 `:5173`;`:3000` 只有在生产模式(Node 渲染服务器)下才有页面。 ### 更新截图 @@ -412,17 +425,20 @@ jiang13-forum/ ├── cmd/jiang13/ # 程序入口(含系统服务注册) ├── config/ # app.ini 与命令行配置 ├── app.ini.example # 配置文件示例 -├── Dockerfile # 多阶段 Docker 构建 -├── docker-compose.yml # 单容器 Compose 部署 -├── docker-entrypoint.sh # 容器启动脚本(修正 /data 卷权限) +├── Dockerfile # 多阶段构建(前端 + Go API 共用镜像,双容器运行) +├── docker-compose.yml # 双容器 Compose 部署(web :3000 + api :3001) +├── docker-entrypoint.sh # api 容器启动脚本(修正 /data 卷权限) ├── .dockerignore ├── model/ # GORM 模型与数据库迁移 ├── service/ # 业务逻辑 -├── handler/ # HTTP 处理器(前台 + 后台) +├── handler/ # HTTP 处理器(API + /api/home-boot SSR 聚合端点) ├── middleware/ # JWT 鉴权等 -├── router/ # 路由注册 -├── embed_static/ # go:embed 内嵌的 SPA -├── frontend/ # React 源码(Vite 构建) +├── router/ # 路由注册(仅 API) +├── frontend/ +│ ├── pages/ # Vike 页面(feed=SSR 白名单 / spa=catch-all 壳 / _error) +│ ├── renderer/ # Vike 双端渲染入口(onRenderHtml / onRenderClient) +│ ├── server/ # Node 渲染服务器(express + renderPage + API 反代) +│ └── src/ # React 源码(含 src/ssr 服务端取数与作用域管理) ├── docs/screenshots/ # README 界面截图 ├── ROADMAP.md # 路线图与已知问题 └── scripts/ # 开发辅助脚本(含截图) diff --git a/build.ps1 b/build.ps1 index 7e024e0..db3e343 100644 --- a/build.ps1 +++ b/build.ps1 @@ -3,7 +3,7 @@ # .\build.ps1 -Target build-windows param( - [ValidateSet('build', 'build-windows', 'build-linux', 'build-darwin', 'build-all', 'frontend', 'tidy', 'run', 'dev', 'clean', 'docker', 'compose-up', 'compose-down', 'help')] + [ValidateSet('build', 'web-build', 'frontend', 'build-windows', 'build-linux', 'build-darwin', 'build-all', 'tidy', 'run', 'serve', 'dev', 'clean', 'docker', 'compose-up', 'compose-down', 'help')] [string]$Target = 'build' ) @@ -23,7 +23,7 @@ function Ensure-Dir($path) { } function Build-Frontend { - Write-Host '[frontend] npm run build...' -ForegroundColor Cyan + Write-Host '[frontend] npm run build (Vike: client + server)...' -ForegroundColor Cyan Push-Location frontend try { if (-not (Test-Path node_modules)) { @@ -69,13 +69,14 @@ function Build-Go([string]$OutFile, [string]$GoOS = '', [string]$GoArch = '') { switch ($Target) { 'help' { - Write-Host '.\build.ps1 build current platform' - Write-Host '.\build.ps1 -Target frontend frontend only' + Write-Host '.\build.ps1 build Go API binary (current platform)' + Write-Host '.\build.ps1 -Target web-build build frontend (Vike client + server)' + Write-Host '.\build.ps1 -Target serve production run: Node :3000 + Go API :3001' Write-Host '.\build.ps1 -Target build-windows' Write-Host '.\build.ps1 -Target build-linux' Write-Host '.\build.ps1 -Target build-all' - Write-Host '.\build.ps1 -Target run backend only (port 3000)' - Write-Host '.\build.ps1 -Target dev backend + Vite HMR (recommended for frontend dev)' + Write-Host '.\build.ps1 -Target run Go API only (port 3000, no pages)' + Write-Host '.\build.ps1 -Target dev Vike HMR :5173 + Go API :3000 (recommended for frontend dev)' Write-Host '.\build.ps1 -Target tidy' Write-Host '.\build.ps1 -Target clean' Write-Host '.\build.ps1 -Target docker build Docker image' @@ -84,6 +85,7 @@ switch ($Target) { Write-Host '' Write-Host 'Note: Windows "make" is often Embarcadero MAKE, not GNU Make.' } + 'web-build' { Build-Frontend } 'frontend' { Build-Frontend } 'tidy' { go mod tidy } 'clean' { @@ -94,11 +96,35 @@ switch ($Target) { Ensure-Dir $DevDataDir go run $MainPkg --data $DevDataDir } + 'serve' { + Build-Frontend + Build-Go -OutFile $AppName + Ensure-Dir $DevDataDir + $root = (Get-Location).Path + $goExe = Join-Path $root "$BuildDir\$AppName.exe" + Write-Host '[serve] 正在新窗口启动 Go API (:3001)...' -ForegroundColor Cyan + Start-Process powershell -ArgumentList @( + '-NoExit', '-Command', + "Set-Location '$root'; & '$goExe' --data '$DevDataDir' --port 3001" + ) | Out-Null + Start-Sleep -Seconds 2 + Write-Host '[serve] 启动 Node 渲染服务器 (:3000)...' -ForegroundColor Cyan + Push-Location frontend + try { + $env:PORT = '3000' + $env:GO_API_URL = 'http://127.0.0.1:3001' + npm start + } finally { + Remove-Item Env:PORT -ErrorAction SilentlyContinue + Remove-Item Env:GO_API_URL -ErrorAction SilentlyContinue + Pop-Location + } + } 'dev' { $root = (Get-Location).Path Ensure-Dir $DevDataDir Write-Host '' - Write-Host '[dev] 前端开发 : http://localhost:5173 (Vite HMR)' -ForegroundColor Green + Write-Host '[dev] 前端开发 : http://localhost:5173 (Vike HMR)' -ForegroundColor Green Write-Host '[dev] 后端 API : http://localhost:3000 (Go)' -ForegroundColor Green Write-Host "[dev] 数据目录 : $DevDataDir (与 dist 二进制一致)" -ForegroundColor Green Write-Host '[dev] 提示 : 请访问 5173 端口,Vite 会自动代理 API 到 3000' -ForegroundColor Yellow @@ -117,24 +143,18 @@ switch ($Target) { } } 'build' { - Build-Frontend Build-Go -OutFile $AppName } 'build-windows' { - Build-Frontend Build-Go -OutFile $AppName -GoOS 'windows' -GoArch 'amd64' } 'build-linux' { - Write-Host '[build-linux] will npm run build then go:embed SPA' -ForegroundColor Yellow - Build-Frontend Build-Go -OutFile "$AppName-linux-amd64" -GoOS 'linux' -GoArch 'amd64' } 'build-darwin' { - Build-Frontend Build-Go -OutFile "$AppName-darwin-arm64" -GoOS 'darwin' -GoArch 'arm64' } 'build-all' { - Build-Frontend Build-Go -OutFile $AppName -GoOS 'windows' -GoArch 'amd64' Build-Go -OutFile "$AppName-linux-amd64" -GoOS 'linux' -GoArch 'amd64' Build-Go -OutFile "$AppName-darwin-arm64" -GoOS 'darwin' -GoArch 'arm64' diff --git a/docker-compose.yml b/docker-compose.yml index 51ab7b4..f80b2ac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,17 @@ -# 姜十三论坛 — Docker Compose 单服务部署 +# 姜十三论坛 — Docker Compose 双服务部署 +# 架构:web(Node :3000 对外:Vike SSR + 静态资源 + API 反代) → api(Go :3001 内部:纯 API + /data 数据卷) # 启动:docker compose up -d --build # 或:make compose-up / build.bat -Target compose-up services: - jiang13: + api: image: hangzhang714128/jiang13-forum:latest build: context: . dockerfile: Dockerfile args: VERSION: dev - ports: - - "3000:3000" + command: ["--work-path", "/app", "--data", "/data", "--port", "3001"] volumes: - jiang13-data:/data # 可选:挂载自定义 app.ini(只读) @@ -20,10 +20,34 @@ services: TZ: Asia/Shanghai # 可选:固定 JWT 密钥(留空则自动生成并持久化到 /data/.jwt_secret) # JIANG13_JWT_SECRET: your-secret-here + expose: + - "3001" restart: unless-stopped stop_grace_period: 15s healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"] + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3001/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s + + web: + image: hangzhang714128/jiang13-forum:latest + entrypoint: ["node", "server/index.mjs"] + working_dir: /app/frontend + user: "1000:1000" + ports: + - "3000:3000" + environment: + TZ: Asia/Shanghai + PORT: "3000" + GO_API_URL: http://api:3001 + depends_on: + api: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 30s timeout: 5s retries: 3 diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index bbdbe8e..9a9f7f2 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,15 +1,16 @@ #!/bin/sh set -e +# api 容器入口:默认运行 Go API(web 容器在 compose 中以 node 用户直接覆盖 entrypoint)。 # 宿主机 / 1Panel 挂载的 volume 常为 root 所有(uid 0), -# 而应用以 jiang13(uid 1000)运行,需在启动时修正 /data 权限后降权。 +# 而应用以 uid 1000(node 镜像内置用户)运行,需在启动时修正 /data 权限后降权。 DATA_DIR="/data" if [ "$(id -u)" = '0' ]; then mkdir -p "$DATA_DIR" - chown -R jiang13:jiang13 "$DATA_DIR" - exec su-exec jiang13 /app/jiang13 "$@" + chown -R 1000:1000 "$DATA_DIR" + exec gosu 1000:1000 /app/jiang13 "$@" fi exec /app/jiang13 "$@"