NestJS项目部署到阿里云ECS:从环境搭建到Nginx与PM2的完整实战 📅 发布时间:2026/9/16 16:22:58 👁 浏览次数: 上个星期把一个 NestJS 项目从本地开发环境搬到阿里云 ECS 上前后折腾了两天多。项目本身不算复杂——一个基于 NestJS TypeORM MySQL 的后端服务带 Socket.IO 实时通信模块本地跑得顺风顺水一上服务器就各种妖魔鬼怪端口不通、502、WebSocket 握手失败、数据库连接超时、文件上传权限问题……每一步都像是盲人摸象。折腾完回头整理了一下笔记发现踩的坑其实都很典型决定写成一篇完整的部署实录。这篇文章会从方案设计、服务器环境搭建、NestJS 构建与进程守护、Nginx 反向代理到具体报错的排查思路一条线讲清楚。内容比较长适合已经会用 NestJS 写业务、但第一次独立部署到云服务器的朋友。如果你是完全没碰过 Linux 的新手建议先把基本命令过一遍再来看效果会好很多。1. 部署方案设计先想清楚再买服务器1.1 为什么选 ECS 而不是其他部署方式动手之前我在阿里云的一众部署方案里纠结了很久。函数计算 FC 确实轻量按调用次数计费但 NestJS 这种常驻型服务尤其还依赖 WebSocket 长连接在函数计算上跑特别别扭连接状态的管理非常麻烦。ACK 容器服务和 SAE 应用引擎我也看了前者对单机小项目来说太重光理解 Pod、Service、Ingress 这一套概念就够喝一壶后者省心但调试链路不透明出了问题不好排查。最终选了 ECS 自建理由很直接可控性最强系统、运行时、进程管理全部自己说了算成本也相对可控包年包月一台 2C4G 的实例新用户活动价折算下来比云托管便宜不少最重要的是部署路径清晰NestJS 跑 Node 进程前面挂 Nginx 做反向代理这是社区里验证过无数次的成熟玩法网上踩坑资料也最多。如果你以后要上 Docker 或者 K8sECS 也能平滑演进——先在裸机部署跑通业务再慢慢容器化不会走弯路。1.2 部署前把技术栈和版本定下来这一条是这次踩坑学到的。部署前一定要把版本确定好不要上了服务器再临场发挥。我最终定的方案是这样的组件版本选择说明操作系统Ubuntu 22.04 LTS稳定、资料多、软件源够新Node.js20.x LTSNestJS 10 要求 1820 是当前稳健之选PM2最新版进程守护、日志管理、开机自启Nginx1.18反向代理 WebSocket 升级 HTTPS 终结MySQL8.0业务主库utf8mb4 字符集Redis7.x缓存 Socket.IO 多实例适配器版本坑我在后面会详细说这里先提醒一句NestJS 10 对 Node 版本有硬性要求如果装上旧的 Node.js 12/14npm install的时候就一堆 engine 警告build大概率直接失败。别问我怎么知道的。部署目录我也提前规划好了避免文件散落到处找/var/www/nest-app 项目代码 /var/log/pm2 PM2 日志 /var/log/nginx Nginx 日志 /data/uploads 文件上传目录2. 服务器环境搭建把地基打牢2.1 实例规格与安全组配置实例规格方面如果只是中小型业务2 核 4G 起步完全够用。我这边的服务高峰期 QPS 不算高CPU 和内存都还有不少余量。如果你的 NestJS 服务要做大量计算或者图片处理建议直接上 4 核 8G别在服务器配置上抠门后面扩容更折腾。系统镜像我选了 Ubuntu 22.04 LTS相比 CentOS 系的镜像软件源更新及时apt装软件省心很多。如果你所在团队习惯了 CentOS/Alibaba Cloud Linux 也没问题命令上稍有差异但思路完全一致。安全组是阿里云区别于普通 VPS 的一个关键点也是我这次第一个大坑。阿里云的 ECS 除了系统内部的防火墙ufw/firewalld还单独有一层安全组控制而且默认只放行了 22、3389 这类端口。你安全组不放行就算在 ECS 里把 NestJS 正常跑起来了外网也完全访问不到。配置路径ECS 控制台 → 实例 → 安全组 → 配置规则 → 入方向 → 手动添加。我按需要放行了这几个端口协议端口授权对象用途TCP220.0.0.0/0SSH 远程连接TCP800.0.0.0/0HTTP 访问TCP4430.0.0.0/0HTTPS 访问TCP30000.0.0.0/0临时调试 NestJS上线后可关注意如果不想让某个端口被全网扫描授权对象可以改成你当前的公网 IP 加 /32比如114.114.114.114/32只允许你自己的 IP 访问。这个对 22 端口尤其重要能少挨很多扫描攻击。2.2 Node.js 环境安装与版本管理Node.js 的安装方式我试过好几种最推荐还是用 nvm 做版本管理。线上环境有时候需要切换 Node 版本排查问题有 nvm 在手进退自如。curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 20.11.1 nvm use 20.11.1 nvm alias default 20.11.1如果服务器拉取脚本超时也可以直接使用 NodeSource 源或者下载官方二进制包解压到/usr/local/效果一样。装完务必验证版本node -v npm -v国内服务器上还有一个必须做的操作把 npm 镜像源切到国内。不换源的话npm install装依赖时速度可能非常感人甚至直接超时失败。npm config set registry https://registry.npmmirror.com2.3 MySQL 与 Redis 安装配置数据库我用的是系统源里的一键安装sudo apt update sudo apt install -y mysql-server redis-server sudo systemctl enable mysql redis sudo systemctl start mysql redisMySQL 8.0 装完后需要初始化这里有个坑默认 root 用的是 auth_socket 认证直接sudo mysql就能进但 TypeORM 连不上。我按下面的方式设置了密码并新建了业务账号sudo mysql ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的强密码; CREATE DATABASE nest_app DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER nestlocalhost IDENTIFIED BY 你的业务密码; GRANT ALL PRIVILEGES ON nest_app.* TO nestlocalhost; FLUSH PRIVILEGES;注意MySQL 8.0 默认的认证插件是caching_sha2_password如果你在用老旧的mysqlnpm 包驱动连接时会报authentication plugin错误。解决方式是换用mysql2驱动TypeORM 配置里设type: mysql并安装 mysql2或者像我上面那样给用户指定mysql_native_password认证。Redis 默认只监听 127.0.0.1这个对于单机部署来说刚刚好不需要改。千万注意别把 Redis 暴露到公网没密码的 Redis 挂在公网上几小时就能被扫描工具薅去挖矿。3. NestJS 项目构建与进程守护3.1 代码发布与依赖安装把代码传到服务器的方式我推荐用 git。项目在私有仓库的话先在服务器上生成 SSH key把公钥加到 Gitee 或者阿里云 Codeup 的仓库里然后直接 clone。如果代码量不大用 scp 从本地传上去也行但后续更新还得手动覆盖不如 git 规范。cd /var/www git clone gitgitee.com:yourname/nest-app.git cd nest-app依赖安装这里有个细节如果只在服务器上跑生产环境npm install --production就够了不会安装 devDependencies。但 NestJS 项目在服务器上执行npm run build是需要 devDependencies 的比如 TypeScript、nestjs/cli所以我个人推荐的做法是本地执行npm run build把dist目录一起传到服务器服务器上只装生产依赖跑dist/main.js。如果项目比较大或者想保持服务器上有完整构建能力这两步都要做npm install npm run build构建产物默认在dist/main.js但有些项目自定义过tsconfig的outDirbuild 后产物路径会不一样以package.json里的 scripts 为准。3.2 环境变量与配置管理这是部署中最容易出错、又最不容易被发现的环节。本地开发环境的.env和生产环境往往不一样数据库地址、密码、JWT 密钥、第三方服务的 Key 都需要切换。我的习惯是仓库里放一个.env.example作为模板服务器上单独创建.envcp .env.example .env vim .env几个容易踩的坑NODE_ENV没有设置成production生产模式下很多日志输出、缓存策略、错误处理行为和开发模式差异很大。数据库host写了localhost但 MySQL 用户授权绑定的 host 是127.0.0.1或者反过来连接就会莫名失败。建议统一写127.0.0.1避免 localhost 在某些环境下被解析成 IPv6 的::1。TypeORM 的synchronize: true在生产环境一定要关掉。开发时它自动建表很爽生产环境一旦实体和数据库不一致可能直接给你改表甚至丢数据。正确姿势是用 migration 管理表结构。另外.env文件不要提交到 git 仓库.gitignore里务必加上。3.3 PM2 进程守护配置直接node dist/main.js起服务有个致命问题SSH 终端一关进程就跟着没了。所以必须用进程守护工具PM2 是这个场景下最成熟的方案。npm install -g pm2 pm2 start dist/main.js --name nest-app --time pm2 save pm2 startuppm2 save会保存当前进程列表pm2 startup会生成一条开机自启命令按它输出的内容执行一遍之后服务器重启PM2 会自动拉起服务。这个一定要做不然 ECS 一重启比如系统迁移、意外重启你的服务就彻底消失了。如果项目需要多个实例或者内存限制我更推荐用ecosystem.config.js配置module.exports { apps: [ { name: nest-app, script: dist/main.js, instances: 2, exec_mode: cluster, max_memory_restart: 512M, out_file: /var/log/pm2/nest-out.log, error_file: /var/log/pm2/nest-error.log, merge_logs: true, time: true, env: { NODE_ENV: production } } ] };然后运行pm2 start ecosystem.config.js这里要提一个和 NestJS 相关的关键坑如果你的服务用了 Socket.IO并且开了 PM2 的 cluster 模式多个进程之间的事件广播会出问题。客户端连接到进程 A事件可能发到进程 B导致某些用户收不到消息。解决方案是给 Socket.IO 配上 Redis Adapter让所有进程共享事件通道。简单起见我这次先跑单实例等真的需要横向扩容了再加。PM2 日常操作命令记一下pm2 status # 查看所有进程状态 pm2 logs nest-app # 实时查看应用日志 pm2 restart nest-app # 重启应用 pm2 reload nest-app # 零停机重载4. Nginx 反向代理与 HTTPS 配置4.1 为什么前端还要套个 Nginx有读者可能疑惑NestJS 自己监听 3000 端口直接让用户访问http://公网IP:3000不就行了吗技术上确实能跑但这样问题很多URL 里带个怪异的端口号不体面NestJS 对静态资源、请求限流、日志访问的处理效率都不如 Nginx 专业最关键的是后续要上 HTTPS 证书用 Nginx 做证书终结和 TLS 卸载是最标准也最灵活的做法。所以说白了Nginx 在前面做代理NestJS 在后面专心处理业务各司其职。4.2 站点配置与代理参数安装 Nginx 后在/etc/nginx/sites-available/下新建站点配置然后软链到sites-enabledsudo apt install -y nginx sudo vim /etc/nginx/sites-available/nest-app.conf基础配置长这样server { listen 80; server_name api.example.com; client_max_body_size 50m; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 5s; proxy_read_timeout 60s; } }这里我踩过三个具体坑proxy_pass后面如果写了http://127.0.0.1:3000/带上末尾斜杠Nginx 会把原始 URI 的路径前缀替换掉和 NestJS 的路由拼接结果会错位。比如请求/api/users可能被转发成//users直接 404。NestJS 项目这里一定不要带斜杠。Nginx 默认client_max_body_size只有 1m如果你后端有文件上传接口超过几 M 的文件直接返回 413 Request Entity Too Large。必须按业务需要调大。不要忘记proxy_set_header Host $host。NestJS 里如果有基于 Host 的校验或者生成绝对 URL 的逻辑少了这个头取到的就是127.0.0.1:3000各种诡异问题都会冒出来。配置完先检查语法再重载sudo nginx -t sudo nginx -s reload4.3 WebSocket 代理NestJS 项目必须处理如果 NestJS 项目有 Socket.IO 模块Nginx 必须支持协议升级否则前端 socket.io 客户端会一直连接中。在location /里加上这两行proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade;我最初就是因为少了这两行浏览器控制台一直报WebSocket connection to wss://... failed纠结了很久才发现是代理层拦截了升级请求。还要注意proxy_read_timeout不能设太短否则 WebSocket 长连接会被 Nginx 掐断。我设的是 60s配合 Socket.IO 默认的心跳机制跑了两天没断过。4.4 SSL 证书的两种选择HTTPS 现在是标配了。阿里云提供免费的 SSL 证书申请在控制台里操作就能拿到但免费证书有有效期限制需要按时去续期。如果想省心我推荐用 certbot 自动申请和续期 Lets Encrypt 证书sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d api.example.comcertbot 会自动修改 Nginx 配置、申请证书、设置续期定时任务几乎是零维护。需要注意的是申请前要把域名 A 记录解析到 ECS 的公网 IP且 80 端口要能正常访问否则验证会失败。5. 踩坑记录从报错到排错的全过程这一节是全文的核心我把这次部署遇到的所有典型问题整理成了一套排错思路按症状分类写。5.1 端口不通安全组是“隐形防火墙”现象在 ECS 上curl http://127.0.0.1:3000返回正常但浏览器访问http://公网IP:3000就是超时。排查链路先确认 NestJS 监听地址是不是0.0.0.0。NestJS 默认app.listen(3000)会监听所有网卡但如果你显式写了app.listen(3000, 127.0.0.1)外网当然不通。检查系统防火墙sudo ufw status如果开了 ufw 且没放行 3000 端口加一条规则。重点查安全组。阿里云 ECS 控制台 → 安全组 → 配置规则看入方向有没有放行对应端口。这一步是最容易被忽略的因为本地 VPS 没有这个概念。这种问题的高效排查工具是telnet 公网IP 3000或者nc -vz 公网IP 3000能直接告诉你 TCP 层通不通避免在后端瞎猜。5.2 Node 版本不对构建直接失败现象npm run build报大量 TS 编译错误或者npm install时出现 engine 相关 warning。原因Ubuntu 系统源里的 nodejs 版本可能很旧20.04 默认源里甚至有 10.x 的版本NestJS 10 对 Node 版本有硬性要求版本太低直接编不过。解决清掉系统自带 Node改用 nvm 安装 Node 20.x LTS。安装前用nvm ls-remote查一下可用的版本选最新的 LTS 就行。5.3 Nginx 502 Bad Gateway现象Nginx 返回 502页面无法访问。排查顺序先看后端进程是否活着pm2 status。如果进程状态是 errored 或者 stopped看pm2 logs找崩溃原因。确认 Nginx 代理地址和端口和 NestJS 一致。NestJS 监听 3000那proxy_pass就写http://127.0.0.1:3000。写proxy_pass时用127.0.0.1而不是localhost。localhost在 Nginx 解析时可能走 IPv6 的::1而 NestJS 只监听了 IPv4 的 127.0.0.1连接就直接失败。检查磁盘空间df -h。磁盘写满会导致 Node 进程无法写入日志、临时文件也会表现为服务不可用。502 的本质是 Nginx 无法从上游拿到有效响应排查重点永远在后端进程能不能访问、能不能响应而不是先怀疑 Nginx。5.4 数据库连接失败现象NestJS 启动日志里报getaddrinfo ENOTFOUND或ECONNREFUSEDTypeORM 初始化失败。排查思路核对.env里数据库host。ECS 上本地装 MySQL 就写127.0.0.1别写localhost。写localhost在某些 Node 版本里会尝试走 IPv6而 MySQL 默认监听 IPv4。确认 MySQL 用户授权范围。新建用户时写的nestlocalhost和连接时用的 host 必须匹配。检查 MySQL 服务状态systemctl status mysql。如果密码里有特殊字符比如、#、$在.env里加引号或 URL 编码不然会被配置解析器吃掉一部分密码自然对不上。5.5 文件上传权限问题现象接口返回 500PM2 日志里报EACCES: permission denied。原因multer 配置的上传目录不存在或者目录的属主不是运行 Node 进程的那个用户。我一直用ubuntu用户跑 PM2但上传目录建在了 root 账号创建的/data/uploads下权限自然不够。解决sudo mkdir -p /data/uploads sudo chown -R ubuntu:ubuntu /data/uploads注意上传目录最好用chown明确归属不要图省事chmod 777。777 目录会让 Web 日志和上传文件处于完全无防护状态被入侵的风险高很多。5.6 WebSocket 连不上现象前端 socket.io 一直connecting或者连上一次后不久自动断开。排查链路Nginx 有没有加Upgrade和Connection upgrade头这是最直接的原因。是否开了 PM2 cluster 多实例且没配 Socket.IO Redis Adapter。多实例下事件分发会乱。proxy_read_timeout是否过短。如果连接几秒钟就断优先怀疑超时设置。确认 NestJS 网关的 CORS 配置。跨域场景下如果origin没配置对握手请求会被拦掉。5.7 踩坑速查表症状可能原因快速解决公网访问超时安全组未放行端口控制台添加安全组入方向规则502 Bad Gateway后端进程未启动或端口不一致pm2 status 核对 Nginx 代理端口413 Request Entity Too LargeNginxclient_max_body_size太小调大该值后 reloadWebSocket 握手失败Nginx 缺 Upgrade/Connection 头添加对应proxy_set_header数据库 ECONNREFUSEDhost / 端口 / 授权不匹配核对.env与 MySQL 用户授权EACCES 上传失败目录不存在或属主不对mkdirchown到运行用户API 响应慢单实例运行 / 无缓存开启 cluster Redis 缓存6. 上线前最好做掉的几个加固动作部署完能访问只是第一步离一个靠谱的生产环境还差好几步。6.1 不要用 root 跑业务进程很多人图省事直接 root 一把梭但一旦应用被入侵攻击者拿到的是整台机器的最高权限。我在 ECS 上创建了普通用户deploy把项目目录归属给它PM2 也用它启动sudo useradd -m -s /bin/bash deploy sudo chown -R deploy:deploy /var/www/nest-app /data/uploads sudo -u deploy pm2 start dist/main.js --name nest-app6.2 安全组最小授权SSH 的 22 端口授权对象改成你常用出口 IP 的/32降低被暴力破解的概率。MySQL 的 3306 端口不要对公网开放。如果开发环境需要远程连数据库用 SSH 隧道或者只允许内网 IP。之前为调试放行的 3000 端口正式上线后记得在安全组里删掉只保留 80/443 和必要的 22。6.3 日志轮转和定时备份PM2 的日志会一直累积时间长了能把磁盘写满。用 pm2-logrotate 模块做轮转pm2 install pm2-logrotate pm2 set pm2-logrotate:max_size 50M pm2 set pm2-logrotate:retain 7数据库备份也要提上日程写个简单的 cron 每天导出0 3 * * * mysqldump -u nest -p密码 nest_app | gzip /data/backup/nest_app_$(date \%F).sql.gz6.4 锁定依赖版本项目里确保提交了package-lock.json。服务器上安装依赖时会按 lock 文件精确安装避免因为某个传递依赖小版本更新导致线上行为不一致。这个坑在 Node 生态里太常见了。7. 部署过程中最深的几点体会这次部署折腾下来最大的体会是部署本身的技术难度不大难的是对整套链路里每个环节的理解以及排查问题时能否按顺序来。很多人一慌就到处乱试反而越搞越乱。我的经验是只要能沿着“代码 → 进程 → 代理 → 安全组 → 域名解析”这条链路一层一层往下定位百分之九十的问题都能找到根源。对我个人来说最值得的一笔投资是先花半小时把安全组、防火墙、Nginx 配置、PM2 状态这些基础概念理清楚后面的排错速度快了不止一倍。如果你正准备把 NestJS 项目搬到 ECS 上我的建议是先把安全组放行和 Node 版本这两个最容易出问题的点确认好再动手写配置。最后再分享一个小技巧第一次部署时先用pm2 start把服务拉起来然后在本机curl http://127.0.0.1:3000确认后端通了再上 Nginx 做代理这样就把问题天然切成了前后两段排查起来会清晰很多。