chore: Docker 双服务 + Makefile/build.ps1 双进程目标 + README 架构更新
- Dockerfile 改为 Node 24 运行时:Vike 产物 + Go API 二进制共用镜像 - docker-compose 双容器:web(:3000 对外 SSR) + api(:3001 内部,持 /data 卷) - entrypoint 改用 gosu 降权(Debian 基础镜像),健康检查用 node fetch - Makefile/build.ps1:build 不再打包前端,新增 serve(本地双进程)与 web-build - README 同步双服务架构、端口/环境变量、部署与开发说明
This commit is contained in:
@@ -14,7 +14,6 @@ tmp-cookie.txt
|
||||
dist/
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
embed_static/static/spa/
|
||||
node_modules/
|
||||
.vite/
|
||||
|
||||
|
||||
39
Dockerfile
39
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"]
|
||||
|
||||
46
Makefile
46
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 启动"
|
||||
|
||||
140
README.md
140
README.md
@@ -13,7 +13,7 @@
|
||||
[](LICENSE)
|
||||
[](https://hub.docker.com/r/hangzhang714128/jiang13-forum)
|
||||
[](go.mod)
|
||||
[](frontend/package.json)
|
||||
[](frontend/package.json)
|
||||
[](#)
|
||||
|
||||
[在线演示](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/ # 开发辅助脚本(含截图)
|
||||
|
||||
46
build.ps1
46
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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "$@"
|
||||
|
||||
Reference in New Issue
Block a user