使用rpm部署Strapi

strapi 是目前我最喜欢的 CMS 系统之一, 因为它可以嵌入到各种前端框架, 并于各种前端技术浑然天成, 这是我喜欢它的原因. 这里大胆预测一些, 它大概会成为下一代 CMS 系统的引领者. 虽然它还很年轻, 但是已经出现王者气象.

正如很多年轻技术一样, 它还有很多不够完善的地方, 今天就它对于 self-hosted 部署方式的不足之处, 我们另辟蹊径介绍一种我正在实践的自动部署方式. 那就是使用 rpm 包进行部署.

Strapi 的大部分功能还是很优秀的, 只是在部署, 尤其是部署到 self-hosted 环境中时显得特别麻烦, 主要是官方文档没有讲这一块, 官方文档花费打理的篇幅在讲如何部署到 strapi cloud, 以及如何部署到一些公有云平台. 在 self-hosted 部署方面官方文档写得也是晦涩难懂. 主要篇幅都在讲数据库如何安装, 云服务器如何创建, 而在打包帮忙讲得特别肤浅.

或许 self-hosted 不是他们推荐的方式, 而 Strapi 着力推广的也是 strapi cloud 部署, 而使用 strapi cloud 部署就特别方便, 一键部署, 这就是比较鸡贼的地方.

本文作为 Strapi 部署文档的补充, 讲介绍一种新的部署或更新 Strapi 的方式, 即使用 rpm 包部署 strapi. 这样做获得的好处, 在目标服务器, 一键安装 strapi 项目, 自动更新启动脚本, 自动讲 strapi 项目设置为自启动.

1. 手动部署

手动部署前, 需要先编译项目

1
2
3

npm run build

另外需要手动创建 server.js 文件, 内容如下:

1
2
3
const strapi = require("@strapi/strapi");
const app = strapi({ distDir: "./dist" });
app.start();

另外还要手动创建一个 tsconfig.json 文件, 内容与系统默认的 tsconfig.json 稍有不同.

如果不包含 tsconfig.json 文件在数据 import, export 的时候将会遇到一些问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14

"compilerOptions": {
"outDir": "dist",
"rootDir": ".",
"allowJs": true //enables the build without .ts files
},

"include": [
"./",
//"./**/*.ts", //将这两行注释掉
//"./**/*.js",
"src/**/*.json"
],

然后将必要的文件拷贝到需要部署的服务器

这里假设我们要部署的组件目录为 strapi-cms, 将生成的整个 dist 目录拷贝到

1
2
3
4
5
6
7
8
9
10
11
12

strapi-cms
├── dist
├── node_modules
├── package.json
├── public
| └── uploads
├── .env
├── favicon.png
├── tsconfig.json
└── server.js

启动应用程序

启动应用程序使用如下命令

1
2
3

node server.js

2. 首先我们需要制作 rpm 安装包

首先强调一点, 制作 rpm 包不是部署 strapi 必须的步骤, 这里只是为了将部署过程自动化. 当然也可以将 strapi 制作成其他任何形式的安装包, 例如 debian 包或 packman 安装包, 以及一些通用的安装包. 总之都是基于手动部署步骤, 并将手动安装过程自动化, 便于将 strapi 纳入 devops 流水线管理.

最终的效果就是开发和测试阶段完成后交付制品即为 rpm 安装包, 在 staging 环境以及生产环境, 我们不必去安装开发依赖包, 只需要安装 nodejs 运行时环境即可.

编译和制作必要文件的过程与手动部署相同, 这里不再赘述.

2.1. 准备 spec 文件

如何制作 rpm 包可以参考如何制作 RPM 安装包 | 鹏叔的技术博客

一旦了解了 RPM 安装包, 剩下的就是编写 spec 文件了

完整的 rpm 包 spec 文件内容粘贴如下, 仅供参考.

这里假定安装包名为 strapi_cms

vim strapi_cms.spec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#This is a RPM spec file for strapi_cms

%define _topdir %{getenv:HOME}/rpmbuild
%define name strapi_cms
%define release %{_build_number}%{?dist}
%define version 1.0.0
%define buildroot %{_topdir}/%{name}-%{version}-root

BuildRoot: %{buildroot}
Summary: your rpm summary
License: commercial
Name: %{name}
Version: %{version}
Release: %{release}.%{_env_name}
Source: %{name}%{version}.tar.gz
Prefix: /usr
Vendor: your domain name or other vendor infor
Group: Applications/Internet

# requires部分不是必须的, 根据项目实际情况添加
Requires(pre): /usr/sbin/useradd, /usr/bin/getent
Requires(postun): /usr/sbin/userdel

%description
Some description goes here to depict your project.

%prep

%build
cd ${WORKSPACE}/strapi_cms/
npm install --legacy-peer-deps
npm run build

%install
[ "${RPM_BUILD_ROOT}" != "/" ] && [ -d "${RPM_BUILD_ROOT}" ] && \
rm -rf "${RPM_BUILD_ROOT}"

mkdir -p ${RPM_BUILD_ROOT}/var/strapi/cms/public/uploads

cp -r ${WORKSPACE}/strapi_cms/dist ${RPM_BUILD_ROOT}/var/strapi/cms/dist
cp -r ${WORKSPACE}/strapi_cms/node_modules ${RPM_BUILD_ROOT}/var/strapi/cms/node_modules
cp ${WORKSPACE}/strapi_cms/package.json ${RPM_BUILD_ROOT}/var/strapi/cms/package.json
cp ${WORKSPACE}/strapi_cms/server.js ${RPM_BUILD_ROOT}/var/strapi/cms/server.js
cp ${WORKSPACE}/strapi_cms/favicon.png ${RPM_BUILD_ROOT}/var/strapi/cms/favicon.png
cp ${WORKSPACE}/strapi_cms/tsconfig.json ${RPM_BUILD_ROOT}/var/strapi/cms/tsconfig.json


%if "%{?_env_name}" == "prod"
cp ${WORKSPACE}/strapi_cms/.env_prod ${RPM_BUILD_ROOT}/var/strapi/cms/.env
%else
cp ${WORKSPACE}/strapi_cms/.env_dev ${RPM_BUILD_ROOT}/var/strapi/cms/.env
%endif

mkdir -p ${RPM_BUILD_ROOT}/var/log/strapi/cms

%clean
[ "${RPM_BUILD_ROOT}" != "/" ] && [ -d "${RPM_BUILD_ROOT}" ] && \
rm -rf "${RPM_BUILD_ROOT}"
cd ${WORKSPACE}/strapi_cms/
rm -rf dist/

%pre
/usr/bin/getent group strapi || /usr/sbin/groupadd -r strapi
/usr/bin/getent passwd strapi || /usr/sbin/useradd -r -s /sbin/nologin strapi -g strapi

%files

%defattr(-,strapi,strapi)
/var/log/strapi/
/var/strapi/cms/

2.2. 制作 rpm 安装文件

将以上 rpm spec 文件保存为 strapi_cms.spec, 放置与项目根目录, 然后运行以下命令开始制作 rpm 安装文件.

1
2
3
4

# 手动运行rpmbuild还需要设置一下WORKSPACE环境变量, 让系统知道项目位置
export WORKSPACE=/home/gitlab-runner/builds/pyswkhQm/0/root/your_project_parent_folder
rpmbuild --define '_env_name dev' --define "_build_number `date '+%y%m%d%H%M'`" -bb strapi_cms.spec

2.3. gitlab pipeline job

为了让整个打包过程自动化, 我定义了一个 gitlab pipeline job, 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
strapi_cms_dev_releaseBuild:
stage: releaseBuild
when: manual
script:
- echo "Creating dev release build for strapi_cms"
- source /etc/profile
- mkdir -pv ~/rpmbuild/{SOURCES,SPEC,RPMS,BUILD,SRPMS,BUILDROOT}
- mkdir -pv /rpm/centos/7/os/
- cd strapi_cms
- rpmbuild --define '_env_name dev' --define "_build_number `date '+%y%m%d%H%M'`" -bb strapi_cms.spec
- # copy rpm package to rpm repository
- rsync -zvr ~/rpmbuild/RPMS/ /rpm/centos/7/os/
environment:
name: staging
variables:
WORKSPACE: ${CI_PROJECT_DIR}

2.4. 安装 strapi_cms

将 strapi_cms rpm 包拷贝到部署机, 进行安装.

1
2
3

rpm -i strapi_cms-1.0.0-2310091804.el7.dev.x86_64.rpm

由于 strapi 依赖 vips 程序, 大多数 linux 发行版默认都没有安装 vips, 所以在安装 strapi_cms 需要手动安装 vips
具体步骤可以参考https://tufora.com/tutorials/linux/general/install-vips-vips-tools-and-vips-devel-libvips-on-centos-7

安装好 strapi_cms 以后, 进入/var/strapi/cms, 启动 strapi_cms

1
2
3

node server.js

启动之前需要确保相应的数据库已经创建, 本文中使用的是 postgresql, 可以使用如下命令创建数据库

1
2
3

createdb -h localhost -p 5432 -U postgres cms_db

确保.env 文件中的连接信息与 postgresql 数据库一致
如果允许外部网络可以访问 strapi cms 需要放开 1337 端口

1
2
sudo firewall-cmd --add-port=1337/tcp --permanent
sudo firewall-cmd --reload

3. 数据更新

如果数据结构或数据发生改变, 需要手动导入数据.

先导出数据

1
2
3
4
# 首先进入工作目录,在本例中为/var/strapi/cms/

cd /var/strapi/cms/
npm run strapi export -- --file my-strapi-export

运行 export 命令时会问是否要输入加密的 key, 可以输入一个简单的加密 key, 例如 123, 然后记住它,在导入数据时需要使用

将数据上传至服务器

然后导入到新创建的数据库中

1
npm run strapi import -- --file my-strapi-export

4. 设置开机启动

设置为开机自启, 需要借助 PM2

启动

1
2
3
4
5

export NODE_ENV=production

pm2 start --name strapi_cms /var/strapi/cms/server.js

设置开机自启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 设置环境变量
vi /etc/profile.d/strapi.sh
# 添加
export NODE_ENV=production

# 保存要在机器重新启动时重新生成的列表
pm2 save

# 生成开机自启动服务
pm2 startup

# enable pm2开机自启
systemctl enable pm2-root

# 查看process 详情
pm2 show process_name

另外一些有用的 pm2 命令

1
2
3
4
5
6
# 查看进程
pm2 list
# 关闭prcess
pm2 stop process_name
# 删除进程
pm2 delete process_name

至此 PM2 配置完成, ssr 默认会监听在 4000 端口, 可以通过如下命令查找端口号

1
grep "process.env\[\"PORT\"\]" /var/your_app/webapp/server/main.js

其他一些有用的 pm2 命令

查看日志某个任务的日志

1
2
3

pm2 log the_process_name

4.1. 配置 Nginx 反向代理

修改/etc/nginx/conf.d/defaut.conf 将之前的配置由如下

1
2
3
4
5
6
7

location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:1337/;
}

5. 问题排查

5.1. issue 1

当安装程序时, 遇到如下错误

1
2
3
4
5
6

rpm -i application-1.0.0-2310091804.el7.dev.x86_64.rpm
error: Failed dependencies:
libvips-cpp.so.42()(64bit) is needed by application-1.0.0-2310091804.el7.dev.x86_64
-bash-4.2$ rpm -i application-1.0.0-2310092051.el7.dev.x86_64.rpm

需要安装 vips, centos7 上可以参考https://tufora.com/tutorials/linux/general/install-vips-vips-tools-and-vips-devel-libvips-on-centos-7

almalinux 8 上安装 libvips:

首先安装 epel yum 源:参考https://blog.csdn.net/u011458874/article/details/119679216 或者https://www.jianshu.com/p/8f35adeeef58

5.2. issue 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$npm run strapi export -- --file my-strapi-export

> strapi-cms@0.1.0 strapi
> strapi export --file my-strapi-export

? Please enter an encryption key [hidden]
Error: Unknown dialect undefined
at getDialectName (<strapi-cms-project-root>\node_modules\@strapi\database\lib\dialects\index.js:36:13)
at getDialect (<strapi-cms-project-root>\node_modules\@strapi\database\lib\dialects\index.js:42:23)
at new Database (<strapi-cms-project-root>\node_modules\@strapi\database\lib\index.js:31:20)
at Database.init (<strapi-cms-project-root>\node_modules\@strapi\database\lib\index.js:121:14)
at Strapi.bootstrap (<strapi-cms-project-root>\node_modules\@strapi\strapi\lib\Strapi.js:433:30)
at Strapi.load (<strapi-cms-project-root>\node_modules\@strapi\strapi\lib\Strapi.js:504:16)
at async createStrapiInstance (<strapi-cms-project-root>\node_modules\@strapi\strapi\lib\commands\utils\data-transfer.js:133:12)
at async module.exports (<strapi-cms-project-root>\node_modules\@strapi\strapi\lib\commands\actions\export\action.js:59:18)

原因:

参考https://github.com/strapi/strapi/issues/13237

This is because the @strapi/typescript-utils/lib/utils/is-using-typescript uses the tsconfig.json file to determine whether you are starting a Typescript project or a Javascript project

解决办法, 在部署时需要包含 tsconfig.json 文件

但是对原有 tsconfig.json 做一些修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14

"compilerOptions": {
"outDir": "dist",
"rootDir": ".",
"allowJs": true //enables the build without .ts files
},

"include": [
"./",
//"./**/*.ts", //将这两行注释掉
//"./**/*.js",
"src/**/*.json"
],

6. 参考文档

Next.js+Strapi+Ubuntu 从 0 到 1 搭建 CMS 内容管理系统(含域名及证书申请教程)

Install vips, vips-tools and vips-devel (libvips) on CentOS 7

如何从开发环境将 Strapi 项目构建到生产环境