nodebbs 0.1.0 → 0.3.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -310
- package/dist/commands/db/import.d.ts +1 -2
- package/dist/commands/db/import.js +73 -45
- package/dist/commands/pack/index.js +60 -44
- package/dist/commands/restart/index.js +13 -7
- package/dist/commands/start/index.d.ts +1 -1
- package/dist/commands/start/index.js +83 -73
- package/dist/commands/upgrade/index.d.ts +8 -0
- package/dist/commands/upgrade/index.js +39 -0
- package/dist/interactive.js +1 -1
- package/dist/templates/docker-compose.build.yml +10 -0
- package/dist/templates/docker-compose.lowmem.yml +25 -14
- package/dist/templates/docker-compose.prod.yml +89 -0
- package/dist/templates/docker-compose.yml +34 -93
- package/dist/templates/dockerignore +120 -0
- package/dist/templates/env +11 -30
- package/dist/utils/docker.d.ts +1 -1
- package/dist/utils/docker.js +46 -10
- package/dist/utils/env.d.ts +6 -1
- package/dist/utils/env.js +90 -16
- package/dist/utils/selection.d.ts +3 -1
- package/dist/utils/selection.js +22 -23
- package/oclif.manifest.json +103 -80
- package/package.json +1 -1
- package/dist/commands/rebuild/index.d.ts +0 -6
- package/dist/commands/rebuild/index.js +0 -11
|
@@ -4,7 +4,8 @@ import * as fs from 'fs';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as os from 'os';
|
|
6
6
|
import { getTemplatePath } from '../../utils/template.js';
|
|
7
|
-
import { checkDocker } from '../../utils/docker.js';
|
|
7
|
+
import { checkDocker, getComposeFiles, runCompose } from '../../utils/docker.js';
|
|
8
|
+
import { getDeploymentMode } from '../../utils/selection.js';
|
|
8
9
|
export default class Pack extends Command {
|
|
9
10
|
static description = '生成离线部署包';
|
|
10
11
|
static flags = {
|
|
@@ -19,61 +20,67 @@ export default class Pack extends Command {
|
|
|
19
20
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nodebbs-pack-'));
|
|
20
21
|
this.log(`正在准备构建环境: ${tmpDir}`);
|
|
21
22
|
try {
|
|
23
|
+
const mode = await getDeploymentMode();
|
|
24
|
+
if (mode !== 'source') {
|
|
25
|
+
this.error('打包命令仅支持源码部署模式。请在 NodeBBS 项目根目录下运行 (需要包含 package.json 和源代码)。');
|
|
26
|
+
}
|
|
22
27
|
// 2. 导出配置文件
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (!fs.existsSync('docker-compose.yml')) {
|
|
29
|
-
// TODO: 从 templates 复制 (简化起见,我们假设用户在项目根目录,或者我们强制要求)
|
|
30
|
-
// 如果没找到,我们实际上无法 build api/web,因为 build context 需要源码。
|
|
31
|
-
// 所以必须要求在项目根目录运行。
|
|
32
|
-
this.error('请在 NodeBBS 项目根目录下运行此命令 (需要包含 docker-compose.yml 和源代码)。');
|
|
28
|
+
// 使用 getComposeFiles 获取准确的配置文件路径 (包含 base 和 override)
|
|
29
|
+
// 默认打包生产环境配置
|
|
30
|
+
const { files, isBuiltIn } = await getComposeFiles('production');
|
|
31
|
+
if (isBuiltIn) {
|
|
32
|
+
console.warn('当前目录未找到 docker-compose.yml,将使用内置模板。');
|
|
33
33
|
}
|
|
34
|
-
// 3.
|
|
35
|
-
this.log('正在构建应用镜像 (
|
|
36
|
-
|
|
34
|
+
// 3. 构建应用镜像
|
|
35
|
+
this.log('正在构建应用镜像 (这可能需要几分钟)...');
|
|
36
|
+
// 注意:如果使用内置模板,context 可能是错的,但 build 命令通常需要 context。
|
|
37
|
+
// getComposeFiles 返回的 file path 是绝对路径。
|
|
38
|
+
// 如果是 isBuiltIn,说明用的是 cli/templates/docker-compose.yml
|
|
39
|
+
// 在 source 模式下,如果必须用内置模板作为 base,我们需要确保 build context 正确。
|
|
40
|
+
// 如果我们只是为了获取 content,那没问题。但 docker compose build 需要 context。
|
|
41
|
+
// 我们之前 manual check 的时候,如果 missing local file, we fallback to template path.
|
|
42
|
+
// runCompose 会 handle -f arguments.
|
|
43
|
+
// 这里为了简单,我们直接运行 build。
|
|
44
|
+
// 但要注意,如果 isBuiltIn,runCompose 会自动加 --project-directory cwd
|
|
45
|
+
// 这里我们主要目的是打包,所以 build 应该基于 cwd。
|
|
46
|
+
// 原有逻辑直接 execa('docker', ['compose', 'build']),这依赖于 cwd 下有 docker-compose.yml。
|
|
47
|
+
// 如果 cwd 下没有,我们需要传 -f。
|
|
48
|
+
// 所以这里应该使用 runCompose 来执行 build,以确保参数一致性
|
|
49
|
+
await runCompose(files, ['build'], isBuiltIn);
|
|
37
50
|
// 4. 拉取依赖镜像
|
|
38
51
|
this.log('正在确保数据库镜像已下载...');
|
|
39
52
|
await execa('docker', ['pull', 'postgres:16-alpine'], { stdio: 'inherit' });
|
|
40
53
|
await execa('docker', ['pull', 'redis:7-alpine'], { stdio: 'inherit' });
|
|
41
54
|
// 5. 导出镜像
|
|
42
55
|
this.log('正在导出镜像到文件 (nodebbs-images.tar)...');
|
|
43
|
-
// 获取实际的镜像名,这里假设 docker-compose build 生成的镜像名符合预期
|
|
44
|
-
// 通常是 <dir>_api 和 <dir>_web,或者我们在 compose 文件里指定了 image name?
|
|
45
|
-
// 查看 template: 没有指定 image name,所以默认为 ${dirname}-api。
|
|
46
|
-
// 为了确保准确,我们可以解析 docker compose config 的输出,或者强制指定 image name。
|
|
47
|
-
// 更稳妥的方式:直接 save 指定的服务镜像。
|
|
48
|
-
// docker compose images -q 可以获取 ID。
|
|
49
56
|
// 获取所有相关镜像的列表
|
|
50
57
|
const projectName = path.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
51
|
-
|
|
52
|
-
'postgres:16-alpine',
|
|
53
|
-
'redis:7-alpine',
|
|
54
|
-
`${projectName}-api`, // 默认命名规则
|
|
55
|
-
`${projectName}-web`
|
|
56
|
-
];
|
|
57
|
-
// 我们最好通过 docker compose config 来确认镜像名,但这比较复杂。
|
|
58
|
+
// 我们最好通过 docker compose config 来确认镜像名
|
|
58
59
|
// 简单策略:先 save postgres 和 redis,对于 api 和 web,我们先 tag 一下以确保名字固定。
|
|
59
60
|
await execa('docker', ['tag', `${projectName}-api`, 'nodebbs-api:latest']);
|
|
60
61
|
await execa('docker', ['tag', `${projectName}-web`, 'nodebbs-web:latest']);
|
|
61
62
|
const imagesToSave = ['postgres:16-alpine', 'redis:7-alpine', 'nodebbs-api:latest', 'nodebbs-web:latest'];
|
|
62
63
|
await execa('docker', ['save', '-o', path.join(tmpDir, 'nodebbs-images.tar'), ...imagesToSave], { stdio: 'inherit' });
|
|
63
|
-
// 6. 复制配置文件 (并进行修改)
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
let composeContent = fs.readFileSync(
|
|
64
|
+
// 6. 复制配置文件 (并进行修改) - 移除源码挂载,适配离线环境
|
|
65
|
+
// files[0] -> Base
|
|
66
|
+
// files[1] -> Override (Optional)
|
|
67
|
+
const baseFile = files[0];
|
|
68
|
+
let composeContent = fs.readFileSync(baseFile, 'utf-8');
|
|
68
69
|
const lines = composeContent.split('\n');
|
|
69
70
|
const cleanedLines = lines.map(line => {
|
|
70
71
|
// 移除 api 和 web 的源码挂载
|
|
71
|
-
if (line.
|
|
72
|
+
if (line.match(/- \.\/apps\/.*\/src:\/app\/apps\/.*\/src/)) {
|
|
72
73
|
return line.replace(/^/, '# [OFFLINE-PACK-REMOVED] ');
|
|
73
74
|
}
|
|
74
75
|
return line;
|
|
75
76
|
});
|
|
76
77
|
fs.writeFileSync(path.join(tmpDir, 'docker-compose.yml'), cleanedLines.join('\n'));
|
|
78
|
+
// 处理 Override 文件 (files[1])
|
|
79
|
+
// 我们的 install.sh 期望 docker-compose.prod.yml
|
|
80
|
+
if (files.length > 1) {
|
|
81
|
+
const overrideFile = files[1];
|
|
82
|
+
fs.copyFileSync(overrideFile, path.join(tmpDir, 'docker-compose.prod.yml'));
|
|
83
|
+
}
|
|
77
84
|
if (fs.existsSync('.env')) {
|
|
78
85
|
this.warn('检测到 .env 文件,出于安全考虑,不会默认打包 .env 文件。请在部署时手动配置环境变量。');
|
|
79
86
|
}
|
|
@@ -97,14 +104,9 @@ if [ ! -f .env ]; then
|
|
|
97
104
|
echo "警告: 未找到 .env 文件,将使用默认配置或报错。请先复制 .env.example 为 .env 并修改配置。"
|
|
98
105
|
fi
|
|
99
106
|
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
# 这里的难点是 compose file 里写的是 build: .
|
|
104
|
-
# 离线环境没法 build。
|
|
105
|
-
# 解决方案:生成一个专门的 docker-compose.offline.yml
|
|
106
|
-
|
|
107
|
-
cat > docker-compose.override.yml <<EOF
|
|
107
|
+
# 启动服务 (强制使用离线镜像)
|
|
108
|
+
# 我们需要覆盖 build 配置,强制使用 image
|
|
109
|
+
cat > docker-compose.offline.yml <<EOF
|
|
108
110
|
services:
|
|
109
111
|
api:
|
|
110
112
|
build: !reset
|
|
@@ -114,9 +116,21 @@ services:
|
|
|
114
116
|
image: nodebbs-web:latest
|
|
115
117
|
EOF
|
|
116
118
|
|
|
117
|
-
|
|
119
|
+
# 注意:这里假设第二个文件总是 docker-compose.prod.yml
|
|
120
|
+
# 如果没有 override,命令可能会报错吗?
|
|
121
|
+
# 我们在 pack 的时候,只要 files > 1,就生成了 docker-compose.prod.yml
|
|
122
|
+
# 如果只有 base,install.sh 里的 -f docker-compose.prod.yml 会报错。
|
|
123
|
+
# 应该动态生成 install.sh 或者在这里加判断。
|
|
124
|
+
# 简单起见,我们确保 docker-compose.prod.yml 存在 (即使只是空的内容或者仅包含 version?)
|
|
125
|
+
# 或者在 install.sh 里判断文件是否存在。
|
|
118
126
|
|
|
119
|
-
docker
|
|
127
|
+
COMPOSE_Args="-f docker-compose.yml"
|
|
128
|
+
if [ -f docker-compose.prod.yml ]; then
|
|
129
|
+
COMPOSE_Args="$COMPOSE_Args -f docker-compose.prod.yml"
|
|
130
|
+
fi
|
|
131
|
+
COMPOSE_Args="$COMPOSE_Args -f docker-compose.offline.yml"
|
|
132
|
+
|
|
133
|
+
docker compose $COMPOSE_Args up -d
|
|
120
134
|
|
|
121
135
|
echo "部署完成!"
|
|
122
136
|
`;
|
|
@@ -137,7 +151,9 @@ echo "部署完成!"
|
|
|
137
151
|
}
|
|
138
152
|
finally {
|
|
139
153
|
// 清理临时目录
|
|
140
|
-
fs.
|
|
154
|
+
if (fs.existsSync(tmpDir)) {
|
|
155
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
156
|
+
}
|
|
141
157
|
}
|
|
142
158
|
}
|
|
143
159
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
import { runCompose, getComposeFiles } from '../../utils/docker.js';
|
|
3
3
|
import { logger } from '../../utils/logger.js';
|
|
4
|
-
import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
|
|
4
|
+
import { EnvFlag, selectEnvironment, getDeploymentMode } from '../../utils/selection.js';
|
|
5
|
+
import { initImageEnv } from '../../utils/env.js';
|
|
5
6
|
export default class Restart extends Command {
|
|
6
|
-
static description = '
|
|
7
|
+
static description = '重启服务';
|
|
7
8
|
static flags = {
|
|
8
9
|
env: EnvFlag,
|
|
9
10
|
};
|
|
@@ -13,12 +14,17 @@ export default class Restart extends Command {
|
|
|
13
14
|
const env = await selectEnvironment(flags.env);
|
|
14
15
|
const { files, isBuiltIn } = await getComposeFiles(env);
|
|
15
16
|
logger.info('正在重启服务 (使用现有镜像)...');
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
// 确保镜像配置存在
|
|
18
|
+
await initImageEnv();
|
|
19
|
+
const mode = await getDeploymentMode();
|
|
20
|
+
logger.info(`环境: ${env} (${mode === 'image' ? '镜像部署' : '源码部署'})`);
|
|
20
21
|
// 3. 不会执行构建 (除非镜像不存在)
|
|
21
|
-
|
|
22
|
+
// 在镜像模式下,我们需要确保不尝试构建 (哪怕镜像丢失,应该是 pull 而不是 build)
|
|
23
|
+
const args = ['up', '-d', '--force-recreate'];
|
|
24
|
+
if (mode === 'image') {
|
|
25
|
+
args.push('--no-build');
|
|
26
|
+
}
|
|
27
|
+
await runCompose(files, args, isBuiltIn);
|
|
22
28
|
logger.success('服务已重启');
|
|
23
29
|
}
|
|
24
30
|
}
|
|
@@ -3,7 +3,7 @@ export default class Start extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static flags: {
|
|
5
5
|
env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
-
|
|
6
|
+
build: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
7
|
};
|
|
8
8
|
run(): Promise<void>;
|
|
9
9
|
}
|
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import { confirm } from '@inquirer/prompts';
|
|
3
3
|
import { checkDocker, runCompose, waitForHealth, execCompose, getComposeFiles } from '../../utils/docker.js';
|
|
4
|
-
import { initEnv, checkEnv } from '../../utils/env.js';
|
|
4
|
+
import { initEnv, checkEnv, initDockerIgnore, initImageEnv } from '../../utils/env.js';
|
|
5
5
|
import { logger } from '../../utils/logger.js';
|
|
6
6
|
import dotenv from 'dotenv';
|
|
7
|
-
import {
|
|
8
|
-
import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
|
|
7
|
+
import { EnvFlag, selectEnvironment, getDeploymentMode } from '../../utils/selection.js';
|
|
9
8
|
export default class Start extends Command {
|
|
10
|
-
static description = '
|
|
9
|
+
static description = '启动服务';
|
|
11
10
|
static flags = {
|
|
12
11
|
env: EnvFlag,
|
|
13
|
-
|
|
14
|
-
char: '
|
|
15
|
-
description: '
|
|
12
|
+
build: Flags.boolean({
|
|
13
|
+
char: 'b',
|
|
14
|
+
description: '重新构建并启动服务 (跳过健康检查和数据初始化)',
|
|
15
|
+
default: false,
|
|
16
16
|
}),
|
|
17
17
|
};
|
|
18
18
|
async run() {
|
|
19
19
|
const { flags } = await this.parse(Start);
|
|
20
|
-
|
|
20
|
+
if (flags.build) {
|
|
21
|
+
logger.header('NodeBBS 重新构建启动');
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
logger.header('NodeBBS Docker 部署');
|
|
25
|
+
}
|
|
21
26
|
// 1. 选择环境
|
|
22
27
|
const env = await selectEnvironment(flags.env);
|
|
23
28
|
// 2. 获取 Compose 文件
|
|
@@ -34,83 +39,88 @@ export default class Start extends Command {
|
|
|
34
39
|
else if (env === 'lowmem') {
|
|
35
40
|
logger.success('已选择:低配环境');
|
|
36
41
|
}
|
|
42
|
+
else {
|
|
43
|
+
logger.success('已选择:基础环境');
|
|
44
|
+
if (!flags.build) {
|
|
45
|
+
logger.warning('注意:无资源限制,不推荐用于生产环境。');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
37
48
|
// 3. 检查 Docker 和环境变量
|
|
38
49
|
await checkDocker();
|
|
39
50
|
// initEnv 保证 .env 存在(或退出)
|
|
40
51
|
await initEnv();
|
|
52
|
+
// 确保 .dockerignore 存在
|
|
53
|
+
await initDockerIgnore();
|
|
54
|
+
await initImageEnv();
|
|
41
55
|
await checkEnv(env);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!tag) {
|
|
47
|
-
tag = await input({
|
|
48
|
-
message: '请选择要部署的镜像版本:',
|
|
49
|
-
default: 'latest'
|
|
56
|
+
if (!flags.build) {
|
|
57
|
+
const continueDeploy = await confirm({
|
|
58
|
+
message: '是否继续启动?',
|
|
59
|
+
default: true
|
|
50
60
|
});
|
|
61
|
+
if (!continueDeploy) {
|
|
62
|
+
logger.info('操作已取消。');
|
|
63
|
+
this.exit(0);
|
|
64
|
+
}
|
|
51
65
|
}
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
// 4. 构建并启动服务
|
|
67
|
+
const mode = await getDeploymentMode();
|
|
68
|
+
if (flags.build) {
|
|
69
|
+
if (mode === 'image') {
|
|
70
|
+
logger.warning('镜像部署模式下不支持重新构建,将尝试拉取最新镜像...');
|
|
71
|
+
await runCompose(composeFiles, ['pull'], isBuiltIn);
|
|
72
|
+
await runCompose(composeFiles, ['up', '-d'], isBuiltIn);
|
|
73
|
+
logger.success('服务已更新并启动');
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
logger.info('正在重新构建并启动服务...');
|
|
77
|
+
await runCompose(composeFiles, ['up', '-d', '--build'], isBuiltIn);
|
|
78
|
+
logger.success('服务已重新构建并启动');
|
|
64
79
|
}
|
|
65
|
-
return `${parts.join(':')}:${newTag}`;
|
|
66
|
-
};
|
|
67
|
-
process.env.API_IMAGE = replaceTag(currentApiImage, tag);
|
|
68
|
-
process.env.WEB_IMAGE = replaceTag(currentWebImage, tag);
|
|
69
|
-
logger.info(`将部署版本: ${tag}`);
|
|
70
|
-
logger.info(`API Image: ${process.env.API_IMAGE}`);
|
|
71
|
-
logger.info(`Web Image: ${process.env.WEB_IMAGE}`);
|
|
72
|
-
const continueDeploy = await confirm({
|
|
73
|
-
message: '是否继续启动?',
|
|
74
|
-
default: true
|
|
75
|
-
});
|
|
76
|
-
if (!continueDeploy) {
|
|
77
|
-
logger.info('操作已取消。');
|
|
78
|
-
this.exit(0);
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
logger.info('正在推送数据库 schema...');
|
|
95
|
-
await execCompose(composeFiles, 'api', ['npm', 'run', 'db:push'], isBuiltIn);
|
|
96
|
-
logger.success('数据库 schema 推送完成');
|
|
81
|
+
else {
|
|
82
|
+
if (mode === 'source') {
|
|
83
|
+
logger.info('正在构建 Docker 镜像...');
|
|
84
|
+
await runCompose(composeFiles, ['build', '--no-cache'], isBuiltIn);
|
|
85
|
+
logger.success('镜像构建完成');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
logger.info('检测到镜像部署模式,跳过本地构建...');
|
|
89
|
+
// 可选:在这里也可以尝试 pull 一下,或者让 up -d 自动处理
|
|
90
|
+
// await runCompose(composeFiles, ['pull'], isBuiltIn)
|
|
91
|
+
}
|
|
92
|
+
logger.info('正在启动服务...');
|
|
93
|
+
await runCompose(composeFiles, ['up', '-d'], isBuiltIn);
|
|
94
|
+
logger.success('服务启动指令已发送');
|
|
97
95
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
// 5. 启动后操作 (仅完整部署)
|
|
97
|
+
if (!flags.build) {
|
|
98
|
+
await waitForHealth(composeFiles, isBuiltIn);
|
|
99
|
+
const pushDb = await confirm({
|
|
100
|
+
message: '是否推送数据库 schema?',
|
|
101
|
+
default: false
|
|
102
|
+
});
|
|
103
|
+
if (pushDb) {
|
|
104
|
+
logger.info('正在推送数据库 schema...');
|
|
105
|
+
await execCompose(composeFiles, 'api', ['npm', 'run', 'db:push'], isBuiltIn);
|
|
106
|
+
logger.success('数据库 schema 推送完成');
|
|
107
|
+
}
|
|
108
|
+
const seedDb = await confirm({
|
|
109
|
+
message: '是否初始化种子数据?',
|
|
110
|
+
default: false
|
|
111
|
+
});
|
|
112
|
+
if (seedDb) {
|
|
113
|
+
logger.info('正在初始化数据...');
|
|
114
|
+
await execCompose(composeFiles, 'api', ['npm', 'run', 'seed'], isBuiltIn);
|
|
115
|
+
logger.success('数据初始化完成');
|
|
116
|
+
}
|
|
106
117
|
}
|
|
107
|
-
logger.header('NodeBBS 启动成功!');
|
|
118
|
+
logger.header(flags.build ? '启动成功!' : 'NodeBBS 启动成功!');
|
|
108
119
|
// 6. 显示信息
|
|
109
|
-
logger.info(`环境: ${env}`);
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const apiPort = finalEnvConfig.API_PORT || '7100';
|
|
120
|
+
logger.info(`环境: ${env} (${mode === 'image' ? '镜像部署' : '源码部署'})`);
|
|
121
|
+
const envConfig = dotenv.config().parsed || {};
|
|
122
|
+
const webPort = envConfig.WEB_PORT || '3100';
|
|
123
|
+
const apiPort = envConfig.API_PORT || '7100';
|
|
114
124
|
console.log(` Web 前端: http://localhost:${webPort}`);
|
|
115
125
|
console.log(` API 服务: http://localhost:${apiPort}`);
|
|
116
126
|
console.log(` API 文档: http://localhost:${apiPort}/docs`);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Upgrade extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
};
|
|
7
|
+
run(): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { runCompose, getComposeFiles } from '../../utils/docker.js';
|
|
3
|
+
import { logger } from '../../utils/logger.js';
|
|
4
|
+
import { EnvFlag, selectEnvironment, getDeploymentMode } from '../../utils/selection.js';
|
|
5
|
+
import { initImageEnv } from '../../utils/env.js';
|
|
6
|
+
import Start from '../start/index.js';
|
|
7
|
+
export default class Upgrade extends Command {
|
|
8
|
+
static description = '升级服务';
|
|
9
|
+
static flags = {
|
|
10
|
+
env: EnvFlag,
|
|
11
|
+
};
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(Upgrade);
|
|
14
|
+
// 1. 检查部署模式
|
|
15
|
+
const mode = await getDeploymentMode();
|
|
16
|
+
if (mode === 'source') {
|
|
17
|
+
logger.info('当前为源码部署模式,执行重建...');
|
|
18
|
+
const args = ['--build'];
|
|
19
|
+
if (flags.env)
|
|
20
|
+
args.push('--env', flags.env);
|
|
21
|
+
await Start.run(args);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// 2. 镜像模式:拉取更新并重启
|
|
25
|
+
logger.header('NodeBBS 服务升级');
|
|
26
|
+
// 选择环境 (通常需要与运行中一致,这里重新选择以防万一,或者可以复用 restart 的逻辑不传 env 则自选)
|
|
27
|
+
//为了简单,我们遵循 restart 的模式
|
|
28
|
+
const env = await selectEnvironment(flags.env);
|
|
29
|
+
// 确保镜像配置存在
|
|
30
|
+
await initImageEnv();
|
|
31
|
+
const { files, isBuiltIn } = await getComposeFiles(env);
|
|
32
|
+
logger.info('正在拉取最新镜像...');
|
|
33
|
+
await runCompose(files, ['pull'], isBuiltIn);
|
|
34
|
+
logger.success('镜像已更新');
|
|
35
|
+
logger.info('正在重新启动服务...');
|
|
36
|
+
await runCompose(files, ['up', '-d'], isBuiltIn);
|
|
37
|
+
logger.success('服务已升级并启动');
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/interactive.js
CHANGED
|
@@ -29,7 +29,7 @@ export async function runInteractive(root) {
|
|
|
29
29
|
}
|
|
30
30
|
await navigate(tree, [], config);
|
|
31
31
|
}
|
|
32
|
-
const GLOBAL_PRIORITY = ['start', 'stop', 'restart', '
|
|
32
|
+
const GLOBAL_PRIORITY = ['start', 'stop', 'restart', 'upgrade', 'status', 'logs', 'shell', 'db', 'pack'];
|
|
33
33
|
const SCOPED_PRIORITIES = {
|
|
34
34
|
'logs': ['all', 'web', 'api', 'db', 'redis'],
|
|
35
35
|
};
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
# 低内存环境 Docker Compose 覆盖配置
|
|
2
|
+
# 适用于 1C1G 或 1C2G 的低配服务器
|
|
3
|
+
# 使用方式: docker compose -f docker-compose.yml -f docker-compose.lowmem.yml up -d
|
|
4
|
+
|
|
1
5
|
services:
|
|
2
|
-
# PostgreSQL
|
|
6
|
+
# PostgreSQL - 低内存优化
|
|
3
7
|
postgres:
|
|
8
|
+
restart: always
|
|
9
|
+
ports: []
|
|
4
10
|
deploy:
|
|
5
11
|
resources:
|
|
6
12
|
limits:
|
|
@@ -10,16 +16,18 @@ services:
|
|
|
10
16
|
cpus: '0.1'
|
|
11
17
|
memory: 128M
|
|
12
18
|
logging:
|
|
19
|
+
driver: "json-file"
|
|
13
20
|
options:
|
|
14
21
|
max-size: "5m"
|
|
15
22
|
max-file: "2"
|
|
16
23
|
|
|
17
|
-
# Redis
|
|
24
|
+
# Redis - 低内存优化
|
|
18
25
|
redis:
|
|
19
|
-
|
|
26
|
+
restart: always
|
|
27
|
+
ports: []
|
|
20
28
|
command: >
|
|
21
29
|
redis-server
|
|
22
|
-
--requirepass ${REDIS_PASSWORD}
|
|
30
|
+
--requirepass ${REDIS_PASSWORD:-redis_password}
|
|
23
31
|
--appendonly yes
|
|
24
32
|
--appendfsync everysec
|
|
25
33
|
--maxmemory 128mb
|
|
@@ -35,18 +43,17 @@ services:
|
|
|
35
43
|
cpus: '0.1'
|
|
36
44
|
memory: 64M
|
|
37
45
|
logging:
|
|
46
|
+
driver: "json-file"
|
|
38
47
|
options:
|
|
39
48
|
max-size: "5m"
|
|
40
49
|
max-file: "2"
|
|
41
50
|
|
|
42
|
-
# API
|
|
51
|
+
# API - 低内存优化
|
|
43
52
|
api:
|
|
53
|
+
restart: always
|
|
44
54
|
environment:
|
|
45
|
-
#
|
|
55
|
+
# 严格限制 Node.js 内存
|
|
46
56
|
NODE_OPTIONS: "--max-old-space-size=384"
|
|
47
|
-
healthcheck:
|
|
48
|
-
start_period: 90s # 低内存环境启动更慢
|
|
49
|
-
interval: 60s
|
|
50
57
|
deploy:
|
|
51
58
|
resources:
|
|
52
59
|
limits:
|
|
@@ -56,17 +63,17 @@ services:
|
|
|
56
63
|
cpus: '0.2'
|
|
57
64
|
memory: 256M
|
|
58
65
|
logging:
|
|
66
|
+
driver: "json-file"
|
|
59
67
|
options:
|
|
60
68
|
max-size: "10m"
|
|
69
|
+
max-file: "3"
|
|
61
70
|
|
|
62
|
-
# Web
|
|
71
|
+
# Web - 低内存优化
|
|
63
72
|
web:
|
|
73
|
+
restart: always
|
|
64
74
|
environment:
|
|
65
|
-
#
|
|
75
|
+
# 严格限制 Node.js 内存
|
|
66
76
|
NODE_OPTIONS: "--max-old-space-size=384"
|
|
67
|
-
healthcheck:
|
|
68
|
-
start_period: 90s # 低内存环境启动更慢
|
|
69
|
-
interval: 60s
|
|
70
77
|
deploy:
|
|
71
78
|
resources:
|
|
72
79
|
limits:
|
|
@@ -76,5 +83,9 @@ services:
|
|
|
76
83
|
cpus: '0.2'
|
|
77
84
|
memory: 256M
|
|
78
85
|
logging:
|
|
86
|
+
driver: "json-file"
|
|
79
87
|
options:
|
|
80
88
|
max-size: "10m"
|
|
89
|
+
max-file: "3"
|
|
90
|
+
|
|
91
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# 生产环境 Docker Compose 覆盖配置
|
|
2
|
+
# 使用方式: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
|
3
|
+
# 本文件只包含与 docker-compose.yml 的差异部分
|
|
4
|
+
|
|
5
|
+
services:
|
|
6
|
+
# PostgreSQL - 生产环境安全与资源优化
|
|
7
|
+
postgres:
|
|
8
|
+
restart: always
|
|
9
|
+
ports: [] # 安全:不暴露端口到主机
|
|
10
|
+
deploy:
|
|
11
|
+
resources:
|
|
12
|
+
limits:
|
|
13
|
+
cpus: '1'
|
|
14
|
+
memory: 512M
|
|
15
|
+
reservations:
|
|
16
|
+
cpus: '0.25'
|
|
17
|
+
memory: 256M
|
|
18
|
+
logging:
|
|
19
|
+
driver: "json-file"
|
|
20
|
+
options:
|
|
21
|
+
max-size: "10m"
|
|
22
|
+
max-file: "3"
|
|
23
|
+
|
|
24
|
+
# Redis - 生产环境安全与资源优化
|
|
25
|
+
redis:
|
|
26
|
+
restart: always
|
|
27
|
+
ports: [] # 安全:不暴露端口到主机
|
|
28
|
+
command: >
|
|
29
|
+
redis-server
|
|
30
|
+
--requirepass ${REDIS_PASSWORD}
|
|
31
|
+
--appendonly yes
|
|
32
|
+
--appendfsync everysec
|
|
33
|
+
--maxmemory 256mb
|
|
34
|
+
--maxmemory-policy allkeys-lru
|
|
35
|
+
deploy:
|
|
36
|
+
resources:
|
|
37
|
+
limits:
|
|
38
|
+
cpus: '0.5'
|
|
39
|
+
memory: 256M
|
|
40
|
+
reservations:
|
|
41
|
+
cpus: '0.1'
|
|
42
|
+
memory: 128M
|
|
43
|
+
logging:
|
|
44
|
+
driver: "json-file"
|
|
45
|
+
options:
|
|
46
|
+
max-size: "10m"
|
|
47
|
+
max-file: "3"
|
|
48
|
+
|
|
49
|
+
# API - 生产环境资源优化
|
|
50
|
+
api:
|
|
51
|
+
restart: always
|
|
52
|
+
environment:
|
|
53
|
+
# 生产环境特有配置
|
|
54
|
+
NODE_OPTIONS: "--max-old-space-size=512"
|
|
55
|
+
deploy:
|
|
56
|
+
resources:
|
|
57
|
+
limits:
|
|
58
|
+
cpus: '1'
|
|
59
|
+
memory: 768M
|
|
60
|
+
reservations:
|
|
61
|
+
cpus: '0.3'
|
|
62
|
+
memory: 384M
|
|
63
|
+
logging:
|
|
64
|
+
driver: "json-file"
|
|
65
|
+
options:
|
|
66
|
+
max-size: "20m"
|
|
67
|
+
max-file: "5"
|
|
68
|
+
|
|
69
|
+
# Web - 生产环境资源优化
|
|
70
|
+
web:
|
|
71
|
+
restart: always
|
|
72
|
+
environment:
|
|
73
|
+
# 生产环境特有配置
|
|
74
|
+
NODE_OPTIONS: "--max-old-space-size=512"
|
|
75
|
+
deploy:
|
|
76
|
+
resources:
|
|
77
|
+
limits:
|
|
78
|
+
cpus: '1'
|
|
79
|
+
memory: 768M
|
|
80
|
+
reservations:
|
|
81
|
+
cpus: '0.3'
|
|
82
|
+
memory: 384M
|
|
83
|
+
logging:
|
|
84
|
+
driver: "json-file"
|
|
85
|
+
options:
|
|
86
|
+
max-size: "20m"
|
|
87
|
+
max-file: "5"
|
|
88
|
+
|
|
89
|
+
|