feat: add combined frontend and API Docker deployment
This commit is contained in:
1 parent
0b2588e09c
commit
a5f1236d44
12 files changed
+429
-2
No files matched your search
@@ -0,0 +1,13 @@
|
|||||||
|
.git
|
||||||
|
.codex
|
||||||
|
.agents
|
||||||
|
**/node_modules
|
||||||
|
.pnpm-store
|
||||||
|
**/dist
|
||||||
|
**/public
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
**/*.log
|
||||||
|
**/coverage
|
||||||
|
**/*.db
|
||||||
|
**/*.sqlite*
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Copy to .env.production in the project root. Never commit real credentials.
|
||||||
|
# Replace the domain, database connection and initial admin password before startup.
|
||||||
|
NODE_ENV=production
|
||||||
|
DATABASE_URL="mysql://USER:URL_ENCODED_PASSWORD@MYSQL_HOST:3306/worthpath"
|
||||||
|
ADMIN_USERNAME=admin
|
||||||
|
ADMIN_PASSWORD=REPLACE_WITH_A_STRONG_INITIAL_PASSWORD
|
||||||
|
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
PORT=3100
|
||||||
|
API_ALLOWED_HOSTS=worthpath.example.com
|
||||||
|
WEB_ORIGIN=https://worthpath.example.com
|
||||||
|
|
||||||
|
MCP_PUBLIC_URL=https://worthpath.example.com/mcp
|
||||||
|
MCP_WEB_URL=https://worthpath.example.com
|
||||||
|
MCP_ALLOWED_HOSTS=worthpath.example.com
|
||||||
|
MCP_ALLOWED_ORIGINS=https://worthpath.example.com
|
||||||
|
MCP_ALLOW_LOOPBACK_HOSTS=false
|
||||||
|
|
||||||
|
NETWORK_ALLOW_HTTP=false
|
||||||
|
NETWORK_ALLOW_HTTP_REDIRECTS=false
|
||||||
|
NETWORK_ALLOW_WILDCARD_ORIGINS=false
|
||||||
|
NETWORK_REQUIRE_SECURE_COOKIE=true
|
||||||
|
NETWORK_HSTS=true
|
||||||
|
NETWORK_UPGRADE_INSECURE_REQUESTS=true
|
||||||
|
NETWORK_RATE_LIMIT_ENABLED=true
|
||||||
|
COOKIE_SECURE=true
|
||||||
|
COOKIE_SAME_SITE=strict
|
||||||
@@ -4,6 +4,8 @@ coverage/
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
!.env.production.example
|
||||||
|
apps/api/public/
|
||||||
*.log
|
*.log
|
||||||
*.db
|
*.db
|
||||||
*.sqlite*
|
*.sqlite*
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
FROM node:24-bookworm-slim AS base
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends openssl ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
RUN npm install -g pnpm@11.19.0
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM base AS build
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
|
||||||
|
COPY apps/api/package.json apps/api/package.json
|
||||||
|
COPY apps/web/package.json apps/web/package.json
|
||||||
|
COPY apps/api/prisma apps/api/prisma
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
COPY apps ./apps
|
||||||
|
RUN pnpm db:generate && pnpm build \
|
||||||
|
&& cp -r apps/web/dist apps/api/public
|
||||||
|
|
||||||
|
FROM base AS runtime
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
# Preserve pnpm's workspace links and Prisma CLI for explicit release migrations.
|
||||||
|
COPY --from=build --chown=node:node /app/node_modules ./node_modules
|
||||||
|
COPY --from=build --chown=node:node /app/package.json /app/pnpm-workspace.yaml ./
|
||||||
|
COPY --from=build --chown=node:node /app/apps/api/package.json ./apps/api/package.json
|
||||||
|
COPY --from=build --chown=node:node /app/apps/api/node_modules ./apps/api/node_modules
|
||||||
|
COPY --from=build --chown=node:node /app/apps/api/dist ./apps/api/dist
|
||||||
|
COPY --from=build --chown=node:node /app/apps/api/public ./apps/api/public
|
||||||
|
COPY --from=build --chown=node:node /app/apps/api/prisma ./apps/api/prisma
|
||||||
|
COPY --from=build --chown=node:node /app/apps/api/assets ./apps/api/assets
|
||||||
|
COPY --from=build --chown=node:node /app/apps/api/scripts/database.cjs /app/apps/api/scripts/seed-icons.cjs ./apps/api/scripts/
|
||||||
|
WORKDIR /app/apps/api
|
||||||
|
USER node
|
||||||
|
EXPOSE 3100
|
||||||
|
CMD ["node", "dist/main.js"]
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# WorthPath
|
# WorthPath
|
||||||
|
|
||||||
|
Docker 前后端合并容器部署见 [Docker 部署说明](docker.md)。
|
||||||
|
|
||||||
个人资产负债与净资产管理。NestJS + React/Vite + TypeScript + MySQL,支持手机和电脑。Node.js 22.12+、pnpm 11、MySQL 8+。
|
个人资产负债与净资产管理。NestJS + React/Vite + TypeScript + MySQL,支持手机和电脑。Node.js 22.12+、pnpm 11、MySQL 8+。
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"dev": "node scripts/dev.cjs",
|
"dev": "node scripts/dev.cjs",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "tsx --test --test-concurrency=1 test/calculation.test.ts test/update.test.ts test/zip.test.ts test/metals.test.ts ../web/test/i18n.test.ts test/account-deletion.test.ts test/mcp-hosts.test.ts test/mcp-http.test.ts test/network.test.ts ../web/test/quick-entry.test.ts ../web/test/net-worth-chart.test.ts ../web/test/login-preferences.test.ts",
|
"test": "tsx --test --test-concurrency=1 test/calculation.test.ts test/update.test.ts test/zip.test.ts test/metals.test.ts ../web/test/i18n.test.ts test/account-deletion.test.ts test/mcp-hosts.test.ts test/mcp-http.test.ts test/network.test.ts test/web.test.ts ../web/test/quick-entry.test.ts ../web/test/net-worth-chart.test.ts ../web/test/login-preferences.test.ts",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:migrate": "node scripts/database.cjs deploy",
|
"db:migrate": "node scripts/database.cjs deploy",
|
||||||
"db:status": "node scripts/database.cjs status",
|
"db:status": "node scripts/database.cjs status",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { AgentOperations } from './mcp/operations';
|
|||||||
import { AgentFiles } from './mcp/files';
|
import { AgentFiles } from './mcp/files';
|
||||||
import { AgentTransport } from './mcp/transport';
|
import { AgentTransport } from './mcp/transport';
|
||||||
import { AgentManagementController } from './mcp/management';
|
import { AgentManagementController } from './mcp/management';
|
||||||
|
import { installWeb } from './web';
|
||||||
@Catch()
|
@Catch()
|
||||||
class SafeErrors implements ExceptionFilter {
|
class SafeErrors implements ExceptionFilter {
|
||||||
catch(error: unknown, host: ArgumentsHost) {
|
catch(error: unknown, host: ArgumentsHost) {
|
||||||
@@ -132,6 +133,7 @@ async function bootstrap() {
|
|||||||
app.get(AgentTransport).install(app.getHttpAdapter().getInstance());
|
app.get(AgentTransport).install(app.getHttpAdapter().getInstance());
|
||||||
app.useGlobalFilters(new SafeErrors());
|
app.useGlobalFilters(new SafeErrors());
|
||||||
setupOpenApi(app);
|
setupOpenApi(app);
|
||||||
|
installWeb(app.getHttpAdapter().getInstance());
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
await app.listen(Number(process.env.PORT || 3100), network.apiHost);
|
await app.listen(Number(process.env.PORT || 3100), network.apiHost);
|
||||||
console.log('WorthPath API ready');
|
console.log('WorthPath API ready');
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { Express, static as serveStatic } from 'express';
|
||||||
|
|
||||||
|
/** Serve only the current Vite assets and page entries; leave service routes to Nest/OAuth. */
|
||||||
|
export function installWeb(app: Express, directory = join(__dirname, '../public')) {
|
||||||
|
const index = join(directory, 'index.html');
|
||||||
|
if (!existsSync(index)) return false;
|
||||||
|
|
||||||
|
app.use('/assets', serveStatic(join(directory, 'assets'), { index: false, redirect: false }));
|
||||||
|
const pages = new Set(['/', '/agent/authorize', '/agent/operation']);
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (!['GET', 'HEAD'].includes(req.method) || !pages.has(req.path)) return next();
|
||||||
|
res.setHeader('Cache-Control', 'no-store');
|
||||||
|
res.sendFile(index, (error) => {
|
||||||
|
if (error) next(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import express from 'express';
|
||||||
|
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { AddressInfo } from 'node:net';
|
||||||
|
import { installWeb } from '../src/web';
|
||||||
|
|
||||||
|
test('combined web serves pages/assets without swallowing API, MCP, OAuth or missing assets', async () => {
|
||||||
|
const dir = await mkdtemp(join(tmpdir(), 'worthpath-web-'));
|
||||||
|
const app = express();
|
||||||
|
let server: ReturnType<typeof app.listen> | undefined;
|
||||||
|
try {
|
||||||
|
assert.equal(installWeb(app, join(dir, 'missing')), false);
|
||||||
|
await mkdir(join(dir, 'assets'));
|
||||||
|
await writeFile(join(dir, 'index.html'), '<html>WorthPath test page</html>');
|
||||||
|
await writeFile(join(dir, 'assets', 'app.js'), 'window.worthpath = true;');
|
||||||
|
assert.equal(installWeb(app, dir), true);
|
||||||
|
// Register downstream service routes to detect accidental web interception.
|
||||||
|
for (const path of [
|
||||||
|
'/api/probe',
|
||||||
|
'/mcp',
|
||||||
|
'/authorize',
|
||||||
|
'/token',
|
||||||
|
'/register',
|
||||||
|
'/revoke',
|
||||||
|
'/.well-known/oauth-authorization-server',
|
||||||
|
]) {
|
||||||
|
app.all(path, (_req, res) => res.status(401).json({ service: path }));
|
||||||
|
}
|
||||||
|
app.use((_req, res) => res.status(404).json({ missing: true }));
|
||||||
|
server = app.listen(0, '127.0.0.1');
|
||||||
|
await new Promise<void>((resolve) => server!.once('listening', resolve));
|
||||||
|
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
||||||
|
for (const path of [
|
||||||
|
'/',
|
||||||
|
'/?agent_operation=test',
|
||||||
|
'/agent/authorize?agent_authorization=test',
|
||||||
|
'/agent/operation',
|
||||||
|
]) {
|
||||||
|
const result = await fetch(base + path);
|
||||||
|
assert.equal(result.status, 200);
|
||||||
|
assert.match(result.headers.get('content-type') || '', /text\/html/);
|
||||||
|
assert.equal(result.headers.get('cache-control'), 'no-store');
|
||||||
|
assert.match(await result.text(), /WorthPath test page/);
|
||||||
|
}
|
||||||
|
const head = await fetch(base + '/', { method: 'HEAD' });
|
||||||
|
assert.equal(head.status, 200);
|
||||||
|
assert.equal(await head.text(), '');
|
||||||
|
assert.match(await (await fetch(base + '/assets/app.js')).text(), /window.worthpath/);
|
||||||
|
for (const path of ['/assets/missing.js', '/api/unknown', '/unknown']) {
|
||||||
|
assert.equal((await fetch(base + path)).status, 404);
|
||||||
|
}
|
||||||
|
assert.equal((await fetch(base + '/', { method: 'POST' })).status, 404);
|
||||||
|
for (const path of [
|
||||||
|
'/api/probe',
|
||||||
|
'/mcp',
|
||||||
|
'/authorize',
|
||||||
|
'/token',
|
||||||
|
'/register',
|
||||||
|
'/revoke',
|
||||||
|
'/.well-known/oauth-authorization-server',
|
||||||
|
]) {
|
||||||
|
for (const method of ['GET', 'POST']) {
|
||||||
|
const result = await fetch(base + path, { method });
|
||||||
|
assert.equal(result.status, 401);
|
||||||
|
assert.deepEqual(await result.json(), { service: path });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (server) {
|
||||||
|
server.closeAllConnections();
|
||||||
|
await new Promise<void>((resolve, reject) =>
|
||||||
|
server!.close((error) => (error ? reject(error) : resolve())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await rm(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: worthpath:${WORTHPATH_IMAGE_TAG:-local}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
env_file:
|
||||||
|
- .env.production
|
||||||
|
ports:
|
||||||
|
- '127.0.0.1:3100:3100'
|
||||||
|
restart: unless-stopped
|
||||||
|
init: true
|
||||||
|
stop_grace_period: 30s
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: '10m'
|
||||||
|
max-file: '3'
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
# WorthPath Docker 部署
|
||||||
|
|
||||||
|
## 部署结构
|
||||||
|
|
||||||
|
前后端合并为一个应用容器:构建时编译 React/Vite 和 NestJS,将网页产物复制到 `apps/api/public`;运行时只启动 `node dist/main.js`。同一端口提供网页、`/api`、MCP 和 OAuth,不启动 Vite 开发服务。
|
||||||
|
|
||||||
|
```text
|
||||||
|
浏览器 / MCP 客户端
|
||||||
|
↓ HTTPS
|
||||||
|
服务器 Caddy / Nginx
|
||||||
|
↓ http://127.0.0.1:3100
|
||||||
|
WorthPath 应用容器
|
||||||
|
↓
|
||||||
|
已有 MySQL 数据库
|
||||||
|
```
|
||||||
|
|
||||||
|
本仓库的 `compose.yaml` 只管理应用,不创建数据库。需要已有 MySQL 8+、已创建的 `worthpath` 数据库及可用的数据库账号。Compose 使用命名镜像,可通过 `WORTHPATH_IMAGE_TAG` 区分版本。运行镜像保留 Prisma CLI 和相关依赖,以便单独执行迁移,不包含后端源码、测试或前端开发目录。
|
||||||
|
|
||||||
|
## Windows 本机安装 Docker 与构建镜像
|
||||||
|
|
||||||
|
本机开发/构建使用 Docker Desktop 的 WSL 2 后端,服务器运行使用 Docker Engine。你可以选择直接在 Linux 服务器构建,本机安装 Docker 不是服务器部署的前置条件。
|
||||||
|
|
||||||
|
先打开任务管理器 → 性能 → CPU,确认“虚拟化”已启用。如果未启用,需要在 BIOS/UEFI 打开硬件虚拟化。
|
||||||
|
|
||||||
|
以管理员身份打开 PowerShell,安装 WSL(不额外安装 Ubuntu):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
wsl --install --no-distribution
|
||||||
|
```
|
||||||
|
|
||||||
|
按提示重启电脑,然后执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
wsl --update
|
||||||
|
wsl --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker Desktop 要求 WSL 2.1.5 或更新版本。参考 [Microsoft WSL 命令说明](https://learn.microsoft.com/en-us/windows/wsl/basic-commands) 和 [Docker Desktop Windows 安装说明](https://docs.docker.com/desktop/setup/install/windows-install/)。
|
||||||
|
|
||||||
|
从上述 Docker 官方页面下载符合电脑架构的安装包(x64 电脑选择 x86_64),运行安装程序并选择 WSL 2 后端。安装完成后打开 Docker Desktop,完成其首次启动步骤,等待引擎启动,使用 Linux containers。
|
||||||
|
|
||||||
|
重新打开 PowerShell,验证客户端、引擎和 Compose:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker version
|
||||||
|
docker compose version
|
||||||
|
docker info --format '{{.OSType}}'
|
||||||
|
docker run --rm hello-world
|
||||||
|
```
|
||||||
|
|
||||||
|
`docker version` 应同时显示 Client 和 Server,操作系统类型应为 `linux`,hello-world 应正常输出成功说明。只有客户端信息或“无法连接引擎”时,先检查 Docker Desktop 是否已启动。不要在 Windows containers 模式构建本项目。
|
||||||
|
|
||||||
|
然后在项目目录只构建镜像,不需要数据库配置,也不会执行数据库迁移:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd E:\WorthPath
|
||||||
|
docker build -t worthpath:local .
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 Docker 能运行但构建下载失败,检查错误对应的镜像仓库/npm/Debian 源网络和 Docker Desktop 代理配置,避免修改业务数据库配置来解决下载问题。
|
||||||
|
|
||||||
|
本机与服务器架构不同时,例如本机 x64、服务器 ARM64,需要按服务器架构构建镜像;上述默认命令按当前 Docker 引擎架构构建。服务器直接构建可以减少架构不匹配的问题。
|
||||||
|
|
||||||
|
## 当前验证记录
|
||||||
|
|
||||||
|
2026-10-05:已通过前后端 TypeScript 检查、前后端生产构建,以及网页托管/网络/Host 的 6 项回归测试。检查了真实环境文件的 Git 忽略规则。测试不连接业务数据库。
|
||||||
|
|
||||||
|
当前本机未找到 Docker 命令,WSL 状态检查提示需要安装;因此尚未完成 Linux 镜像构建、Compose 配置验证、容器内迁移或真实 HTTPS/MCP 联通验证。安装完成后先运行上面的环境验证和 `docker build`,再按后续步骤部署到服务器。
|
||||||
|
|
||||||
|
## 1. 准备服务器
|
||||||
|
|
||||||
|
建议使用 Linux 服务器,安装 Docker Engine 和 Docker Compose 插件,准备可用域名及 HTTPS 反向代理。以下部署命令在服务器的项目根目录执行,使用 Linux shell。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker --version
|
||||||
|
docker compose version
|
||||||
|
```
|
||||||
|
|
||||||
|
服务器构建镜像需要访问镜像仓库、npm 包仓库和 Debian 软件源。镜像中固定 pnpm 11.19.0,并使用锁文件安装依赖;Node 基础镜像为 `node:24-bookworm-slim`,需要严格复现时另行固定镜像 digest。
|
||||||
|
|
||||||
|
## 2. 配置生产环境
|
||||||
|
|
||||||
|
首次部署复制模板;已有配置时不要再次覆盖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -n .env.production.example .env.production
|
||||||
|
chmod 600 .env.production
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑根目录 `.env.production`,至少替换:
|
||||||
|
|
||||||
|
| 配置 | 填写内容 |
|
||||||
|
| ---------------------------------------- | ----------------------------------- |
|
||||||
|
| `DATABASE_URL` | 服务器实际可访问的 MySQL 连接地址 |
|
||||||
|
| `ADMIN_PASSWORD` | 首次初始化管理员使用的强密码 |
|
||||||
|
| `API_ALLOWED_HOSTS`、`MCP_ALLOWED_HOSTS` | 真实域名;非默认端口需要包含端口 |
|
||||||
|
| `WEB_ORIGIN`、`MCP_ALLOWED_ORIGINS` | 网站完整 HTTPS 来源,通常无结尾斜杠 |
|
||||||
|
| `MCP_PUBLIC_URL` | `https://你的域名/mcp` |
|
||||||
|
| `MCP_WEB_URL` | `https://你的域名` |
|
||||||
|
|
||||||
|
模板的生产安全开关已经关闭 HTTP 和通配来源,并开启 Secure Cookie。不要直接沿用 `apps/api/.env` 的开放开发配置。根目录 `.env.production` 由 Compose 注入容器,镜像不携带真实 `.env`;本地 `apps/api/.env` 不会被修改。
|
||||||
|
|
||||||
|
数据库密码中的特殊字符需 URL 编码。容器的 `localhost` 指容器自身,不能用它连接宿主机 MySQL:
|
||||||
|
|
||||||
|
- 独立数据库服务器:填写实际可达地址,允许应用服务器访问。
|
||||||
|
- 同一 Docker 网络的 MySQL:填写数据库服务名,例如 `db`,并将应用加入该网络。
|
||||||
|
- Linux 宿主机 MySQL:可在 Compose 的 `app` 下加 `extra_hosts: ["host.docker.internal:host-gateway"]`,连接地址填 `host.docker.internal`。MySQL 必须监听容器可达的接口,并为容器来源配置账号授权与防火墙;仅监听 `127.0.0.1` 无法连接。
|
||||||
|
|
||||||
|
数据库不必暴露到公网。使用专用账号;迁移账号需要 DDL 权限,长期运行账号可在迁移后改为较小权限。应用不会自动创建数据库。
|
||||||
|
|
||||||
|
## 3. 配置 HTTPS
|
||||||
|
|
||||||
|
默认发布端口为 `127.0.0.1:3100:3100`,供服务器上的反向代理访问,不对外直接开放 3100。
|
||||||
|
|
||||||
|
服务器上安装 Caddy 后,Caddyfile 可使用:
|
||||||
|
|
||||||
|
```caddyfile
|
||||||
|
worthpath.example.com {
|
||||||
|
reverse_proxy 127.0.0.1:3100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
域名解析到服务器,允许访问 80/443。将示例域名替换成环境文件中的同一个域名,再按服务器已有方式验证并加载 Caddy 配置。
|
||||||
|
|
||||||
|
如果已有 Nginx,核心代理配置如下;证书和 HTTPS server 配置由你的服务器提供:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3100;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
client_max_body_size 512m;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
所有路径都代理到应用,包括 `/api`、`/mcp`、OAuth 端点和 `/.well-known/*`。不要只代理 `/api`。代理需支持大文件上传(应用备份最大 512 MB),并保留请求 Host。
|
||||||
|
|
||||||
|
如果反向代理也在 Docker 中,`127.0.0.1` 是代理容器自身。需让两者加入同一 Docker 网络,将上游改为 `app:3100`,并确认仍保留外部 Host。此时可以移除宿主机端口发布。
|
||||||
|
|
||||||
|
当前应用的登录限流存储在进程内,且未配置受信反向代理 IP 解析。代理后的多个用户可能共用限流来源;增加副本不会共享限流计数。较多用户或多副本部署前,需要完善代理信任和共享限流策略。
|
||||||
|
|
||||||
|
## 4. 构建、迁移、启动
|
||||||
|
|
||||||
|
下面命令会修改所配置的数据库。已有数据先备份,并确认 `DATABASE_URL` 指向正确数据库。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编译前后端,生成 Linux Prisma 客户端;不连接数据库或执行迁移
|
||||||
|
docker compose build app
|
||||||
|
|
||||||
|
# 使用一次性容器执行 Prisma migrate deploy;失败后不要继续启动
|
||||||
|
docker compose run --rm app node scripts/database.cjs deploy
|
||||||
|
|
||||||
|
# 初始化/更新固定 ID 的内置图标;保留用户财务数据和上传图标
|
||||||
|
docker compose run --rm app node scripts/seed-icons.cjs
|
||||||
|
|
||||||
|
# 启动唯一的应用服务
|
||||||
|
docker compose up -d app
|
||||||
|
|
||||||
|
docker compose ps
|
||||||
|
docker compose logs --tail=100 app
|
||||||
|
```
|
||||||
|
|
||||||
|
上述一次性容器与应用使用相同镜像和环境文件。迁移或图标初始化失败时先解决错误;没有自动迁移、重置数据库或启动时自动改密。初始管理员只在数据库没有管理员时创建;首次网页登录后必须改密。部署到新数据库不会带上本机数据。
|
||||||
|
|
||||||
|
## 5. 验证
|
||||||
|
|
||||||
|
按顺序检查:
|
||||||
|
|
||||||
|
1. 容器保持运行,日志显示 `WorthPath API ready`,没有循环重启。
|
||||||
|
2. HTTPS 网站打开正常,浏览器网络面板中的 `/assets/*` JS/CSS 返回 200;刷新页面仍能打开。
|
||||||
|
3. 登录、账户查询正常,登录 Cookie 带 Secure;无需启动 5173 端口的开发服务。
|
||||||
|
4. `/agent/authorize` 和 `/agent/operation` 加上实际授权参数后,显示正确的授权或草稿确认页。
|
||||||
|
5. 使用助手页面生成的新 HTTPS MCP 地址重新接入,验证 OAuth 发现、授权和只读查询。旧本机地址不会自动更新。
|
||||||
|
6. `/api/不存在的路径` 与 `/assets/不存在的文件.js` 不应返回前端 HTML;未授权的服务请求应保持后端错误响应。
|
||||||
|
7. 重启容器后仍可查询原数据;数据保存在 MySQL,应用容器可以替换。
|
||||||
|
|
||||||
|
首页连通性可通过域名检查,也可在服务器本机带正确 Host 检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I https://worthpath.example.com/
|
||||||
|
curl -I -H 'Host: worthpath.example.com' http://127.0.0.1:3100/
|
||||||
|
```
|
||||||
|
|
||||||
|
本机 HTTP 检查只能确认页面服务连通;生产登录和 Secure Cookie 需要通过 HTTPS 验证。
|
||||||
|
|
||||||
|
## 6. 更新与回退
|
||||||
|
|
||||||
|
建议为每次发布使用唯一版本标签,例如:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export WORTHPATH_IMAGE_TAG=20261005-1
|
||||||
|
docker compose build app
|
||||||
|
```
|
||||||
|
|
||||||
|
更新顺序:备份数据库 → 保存旧版本标签/镜像 → 更新源码 → 构建新镜像 → 执行迁移 → 按需运行图标初始化 → 替换应用 → 完成上述验证。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose run --rm app node scripts/database.cjs deploy
|
||||||
|
docker compose up -d --no-deps app
|
||||||
|
docker compose logs --tail=100 app
|
||||||
|
```
|
||||||
|
|
||||||
|
回退应用时切换到保留的旧镜像标签,使用 `docker compose up -d --no-build app`。应用镜像回退不会回退数据库迁移;必须确认旧代码兼容新 schema,否则需按已验证的数据库恢复方案处理。
|
||||||
|
|
||||||
|
停止应用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose stop app
|
||||||
|
```
|
||||||
|
|
||||||
|
数据库单独备份到服务器之外,并验证恢复。应用镜像、容器和数据卷均不替代数据库备份;数据库账号、登录授权等完整迁移应采用数据库级迁移方案。个人业务备份导出/恢复仍由用户在网站操作。
|
||||||
|
|
||||||
|
## 7. 本地开发与排查
|
||||||
|
|
||||||
|
原有 `pnpm dev` 保持前后端分开开发;没有 `apps/api/public/index.html` 时,API 不启用网页托管。本地验证合并网页可以先 `pnpm build`,再将 `apps/web/dist` 复制为 `apps/api/public`,此目录已加入 Git/Docker 忽略规则。清理该目录后恢复纯 API 开发入口。
|
||||||
|
|
||||||
|
- 页面 403:检查代理保留的 Host 与 `API_ALLOWED_HOSTS` 是否一致。
|
||||||
|
- MCP Host/Origin 错误:检查固定 HTTPS 地址和白名单,而不是改成 `*`。
|
||||||
|
- 登录 Cookie 未发送:检查使用 HTTPS、`COOKIE_SECURE`、网页来源及浏览器 Cookie 状态。
|
||||||
|
- 数据库连接失败:检查容器可达地址、监听接口、账号来源授权与密码编码,不要打印真实连接字符串。
|
||||||
|
- 缺少表:确认迁移成功,不要使用 reset 处理生产数据。
|
||||||
|
- JS/CSS 404:检查镜像构建完成;如果新增 Vite 的其他顶层静态文件目录,需要同步扩展 `apps/api/src/web.ts` 的托管路径。
|
||||||
|
|
||||||
|
运行镜像包含 Prisma/迁移所需依赖,优先保证发布操作完整;后续可进一步拆分迁移镜像以减小应用镜像。日志设置了轮转,容器使用非 root 用户。镜像构建和服务器端完整验证必须在安装了 Docker 的环境执行。
|
||||||
@@ -6,7 +6,8 @@ if (
|
|||||||
names.some(
|
names.some(
|
||||||
(n) =>
|
(n) =>
|
||||||
/(^|\/)(\.env(?:\..*)?|node_modules|dist|coverage)(\/|$)/.test(n) &&
|
/(^|\/)(\.env(?:\..*)?|node_modules|dist|coverage)(\/|$)/.test(n) &&
|
||||||
!n.endsWith('.env.example'),
|
!n.endsWith('.env.example') &&
|
||||||
|
n !== '.env.production.example',
|
||||||
) ||
|
) ||
|
||||||
names.some((n) => /\.(db|sqlite|log)$/.test(n))
|
names.some((n) => /\.(db|sqlite|log)$/.test(n))
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user