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.
@@ -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
- // 这里我们需要获取当前的 docker-compose.ymlprod 配置
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
- await execa('docker', ['compose', 'build'], { stdio: 'inherit' });
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
- const images = [
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
- // 读取 docker-compose.yml 并移除开发环境的源码挂载
65
- // 这是一个关键步骤,因为在离线/生产环境中,我们不应该挂载 ./apps/xxx/src,
66
- // 否则会覆盖掉镜像内构建好的代码,导致 "Cannot find module" 错误。
67
- let composeContent = fs.readFileSync('docker-compose.yml', 'utf-8');
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.includes('./apps/api/src') || line.includes('./apps/web/src')) {
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
- # 修改 image 名称以匹配 load 进来的 tag (如果 compose 文件里没写 image,它会尝试 build)
101
- # 为了避免 build,我们需要修改 docker-compose.yml 或者使用环境变量 override
102
- # 最简单的办法:我们在 install 脚本里,强制用 docker run 或者修改 compose 文件?
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
- COMPOSE_FILES="-f docker-compose.yml -f docker-compose.override.yml"
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 compose \$COMPOSE_FILES up -d
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.rmSync(tmpDir, { recursive: true, force: true });
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 = '重启所有服务 (up --force-recreate)';
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
- logger.info('环境: ' + env);
17
- // 使用 up -d --force-recreate 可以:
18
- // 1. 如果服务不存在,则创建并启动
19
- // 2. 如果服务已存在,则强制重启
17
+ // 确保镜像配置存在
18
+ await initImageEnv();
19
+ const mode = await getDeploymentMode();
20
+ logger.info(`环境: ${env} (${mode === 'image' ? '镜像部署' : '源码部署'})`);
20
21
  // 3. 不会执行构建 (除非镜像不存在)
21
- await runCompose(files, ['up', '-d', '--force-recreate'], isBuiltIn);
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
- tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
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 { input } from '@inquirer/prompts';
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
- tag: Flags.string({
14
- char: 't',
15
- description: 'Image version tag (e.g. latest, v0.1.0)',
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
- logger.header('NodeBBS Docker 部署');
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
- // 先加载环境变量以获取当前的 IMAGE 配置(如果有)
44
- const envConfig = dotenv.config().parsed || {};
45
- let tag = flags.tag;
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
- // 逻辑:如果 .env 里定义了 API_IMAGE,则解析出 registry/name 部分,然后拼接 tag
54
- // 否则使用默认 registry
55
- const defaultApiImage = 'ghcr.io/aiprojecthub/nodebbs-api';
56
- const defaultWebImage = 'ghcr.io/aiprojecthub/nodebbs-web';
57
- let currentApiImage = envConfig.API_IMAGE || defaultApiImage;
58
- let currentWebImage = envConfig.WEB_IMAGE || defaultWebImage;
59
- const replaceTag = (image, newTag) => {
60
- const parts = image.split(':');
61
- if (parts.length > 1 && !parts[parts.length - 1].includes('/')) {
62
- // 最后一个部分不包含 '/',认为是 tag
63
- parts.pop();
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
- // 4. 拉取并启动服务
81
- logger.info('正在拉取最新的 Docker 镜像...');
82
- await runCompose(composeFiles, ['pull'], isBuiltIn);
83
- logger.success('镜像拉取完成');
84
- logger.info('正在启动服务...');
85
- await runCompose(composeFiles, ['up', '-d'], isBuiltIn);
86
- logger.success('服务启动指令已发送');
87
- // 5. 启动后操作
88
- await waitForHealth(composeFiles, isBuiltIn);
89
- const pushDb = await confirm({
90
- message: '是否推送数据库 schema?',
91
- default: false
92
- });
93
- if (pushDb) {
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
- const seedDb = await confirm({
99
- message: '是否初始化种子数据?',
100
- default: false
101
- });
102
- if (seedDb) {
103
- logger.info('正在初始化数据...');
104
- await execCompose(composeFiles, 'api', ['npm', 'run', 'seed'], isBuiltIn);
105
- logger.success('数据初始化完成');
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
- // 复用之前的 envConfig 或重新读取 (这里直接复用即可,或者为了保险重新读取但不要 redeclare)
111
- const finalEnvConfig = dotenv.config().parsed || {};
112
- const webPort = finalEnvConfig.WEB_PORT || '3100';
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
+ }
@@ -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', 'rebuild', 'status', 'logs', 'shell', 'db', 'pack'];
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
  };
@@ -0,0 +1,10 @@
1
+ services:
2
+ api:
3
+ build:
4
+ context: .
5
+ dockerfile: apps/api/Dockerfile
6
+
7
+ web:
8
+ build:
9
+ context: .
10
+ dockerfile: apps/web/Dockerfile
@@ -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
- # 降低 Node.js 内存限制
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
- # 降低 Node.js 内存限制
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
+