PAYATHON 2026

Jenkins 中 npm start & 无法启动应用

支付小周

结论

npm start & 只是让命令在当前 Shell 中后台运行,并没有把应用交给独立的服务管理器。Pipeline 的 sh 步骤结束后,Jenkins 会清理它启动的子进程。如果后台进程仍占用标准输出、标准错误等文件描述符,Jenkins 还可能一直等待。

更稳妥的做法是使用 systemd、PM2 或 Docker 管理应用,Pipeline 只负责构建和触发部署。如果只是临时验证,可以使用 nohup,完整重定向输入输出,并修改 Jenkins 用于识别子进程的 Cookie。

临时解决方案

可以把启动阶段改成:

stage('Run') {
    steps {
        withEnv(['JENKINS_NODE_COOKIE=dontKillMe']) {
            sh '''
                nohup npm start > app.log 2>&1 < /dev/null &
                echo $! > app.pid
            '''
        }
    }
}

其中每一部分都有作用:

  • nohup 让进程忽略终端断开时产生的挂断信号。
  • > app.log 2>&1 把标准输出和标准错误写入文件。
  • < /dev/null 断开标准输入。
  • & 让 Shell 不必等待应用退出。
  • 修改 JENKINS_NODE_COOKIE,可以避免 Jenkins 在步骤或构建结束时把该进程识别为需要清理的子进程。
  • $! 用于保存后台进程的 PID,方便后续停止应用。

还可以加入简单的启动检查,防止应用启动失败,而 Pipeline 仍然显示成功:

stage('Run') {
    steps {
        withEnv(['JENKINS_NODE_COOKIE=dontKillMe']) {
            sh '''
                nohup npm start > app.log 2>&1 < /dev/null &
                echo $! > app.pid

                sleep 5

                if ! kill -0 "$(cat app.pid)" 2>/dev/null; then
                    echo "Application failed to start"
                    cat app.log
                    exit 1
                fi
            '''
        }
    }
}

这种检查只能确认 PID 对应的进程仍然存在。更可靠的办法是请求应用的健康检查接口,例如:

stage('Run') {
    steps {
        withEnv(['JENKINS_NODE_COOKIE=dontKillMe']) {
            sh '''
                nohup npm start > app.log 2>&1 < /dev/null &
                echo $! > app.pid

                for i in $(seq 1 30); do
                    if curl --fail --silent http://127.0.0.1:3000/ > /dev/null; then
                        exit 0
                    fi
                    sleep 1
                done

                echo "Application did not become ready"
                cat app.log
                exit 1
            '''
        }
    }
}

端口和检测地址要根据实际应用调整。

更可靠的部署方式

长期运行的应用不适合直接托管在 Jenkins 的构建 Shell 中。使用 PM2 时,可以这样部署:

stage('Deploy') {
    steps {
        sh '''
            npm ci
            pm2 start npm --name lapay-payment-frontend -- start
            pm2 save
        '''
    }
}

后续部署可以执行:

stage('Deploy') {
    steps {
        sh '''
            npm ci
            pm2 restart lapay-payment-frontend --update-env ||
                pm2 start npm --name lapay-payment-frontend -- start
        '''
    }
}

这要求 Jenkins 执行用户已经安装并配置好 PM2。

如果服务器使用 systemd,可以提前创建服务单元,再让 Jenkins 执行:

stage('Deploy') {
    steps {
        sh 'sudo systemctl restart lapay-payment-frontend'
    }
}

systemd 可以统一管理自动重启、开机启动、日志、权限和进程状态,应用的生命周期也不再依赖某一次 Jenkins 构建。

日志中的 Node.js 版本也需要处理

日志显示当前环境为:

node: v12.22.9
npm: 8.5.1

多个依赖明确要求使用 Node.js 14 或更高版本。例如,react-scripts@5.0.1 的要求是:

node >=14.0.0

npm install 目前只输出了 EBADENGINE 警告,但这不表示应用一定能在 Node.js 12 下正常运行。需要升级 Jenkins Agent 上的 Node.js,并尽量选择仍在维护的 LTS 版本。最终使用哪个版本,还要根据项目的 package.json、锁文件和依赖要求来确定。

如果 Jenkins 配置了 NodeJS Plugin,可以在 Pipeline 中显式指定工具:

pipeline {
    agent any

    tools {
        nodejs 'node-lts'
    }

    stages {
        stage('Install') {
            steps {
                sh 'node --version'
                sh 'npm --version'
                sh 'npm ci'
            }
        }

        stage('Deploy') {
            steps {
                sh 'pm2 restart lapay-payment-frontend --update-env || pm2 start npm --name lapay-payment-frontend -- start'
            }
        }
    }
}

这里的 node-lts 必须与 Jenkins 全局工具配置中的名称一致。

注意事项

React 项目中的 npm start 通常启动的是开发服务器,不适合直接用于正式生产环境。生产部署一般应先执行:

npm ci
npm run build

构建完成后,再由 Nginx、Apache、对象存储或其他静态资源服务器托管构建产物。

此外,直接保存 $! 得到的可能是 npm 包装进程的 PID,而不是最终运行的 Node.js 子进程。因此,通过 PID 文件停止服务并不总是可靠。生产环境更适合使用 systemd、PM2 或容器编排工具来管理进程。

备注:内容仅供参考。