0.0.0.0:3000(容器通过 host.docker.internal 访问宿主机端口)。node .output/server/index.mjs,可用 HOST/PORT 或 NITRO_HOST/NITRO_PORT 控制)。# 安装
npm i -g pm2
# 查看 pm2 版本
pm2 --version
# 查看 pm2 状态
pm2 status

为确保服务器重启后应用自动恢复,需要配置 PM2 开机自启:
# 1. 生成并配置启动脚本
pm2 startup
# 2. 保存当前进程列表
pm2 save
# 测试重启后恢复
sudo reboot
# 重启后检查应用状态
pm2 list
pm2 logs
pm2 startup 行为说明:Root 用户:/etc/systemd/system/pm2-root.service 并启用sudo env PATH=$PATH:/usr/bin /usr/local/lib/node_modules/pm2/bin/pm2 startup systemd -u username --hp /home/username
pm2 startup 只需在服务器初次配置时执行一次pm2 save 在每次应用更新后执行,保持进程列表同步pm2 unstartup systemd 可以移除开机自启配置sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw deny 3000/tcp
确保 docker-compose.yml 已配置:
services:
nginx:
image: nginx:latest
container_name: my-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./html:/usr/share/nginx/html:ro
- ./etc/ssl:/etc/ssl:ro
extra_hosts:
- "host.docker.internal:host-gateway" # 添加这一行,用于容器内访问宿主机端口
restart: unless-stopped
将主站从静态目录切换为反代 Node :
server {
# 主站改为反代到 Node SSR
location / {
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_pass http://host.docker.internal:3000;
}
}
重启:
docker compose -f /root/my-nginx/docker-compose.yml restart nginx
在项目根目录新增 ecosystem.config.cjs:
module.exports = {
apps: [
{
name: 'mhaibaraai.cn',
script: './.output/server/index.mjs',
exec_mode: 'cluster',
instances: 'max',
env: {
HOST: '0.0.0.0',
PORT: '3000',
NITRO_HOST: '0.0.0.0',
NITRO_PORT: '3000'
},
max_memory_restart: '512M',
out_file: './.pm2/out.log',
error_file: './.pm2/error.log',
time: true
}
]
}
首启与持久化:
pm2 start ecosystem.config.cjs
pm2 save
pm2 startup
SSH_PRIVATE_KEY - SSH 私钥SSH_HOST - SSH 主机name: Deploy
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
env:
NODE_OPTIONS: --max_old_space_size=4096
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Prepare build
run: pnpm run dev:prepare
- name: Run build (SSR)
run: pnpm run build
- name: Setup SSH agent
uses: webfactory/ssh-agent@v0.9.1
with:
ssh-private-key: |
${{ secrets.SSH_PRIVATE_KEY }}
- name: Add known_hosts
env:
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_PORT: 22
run: |
mkdir -p ~/.ssh
ssh-keyscan -H "$SSH_HOST" -p "$SSH_PORT" >> ~/.ssh/known_hosts
- name: Deploy to server via SSH
env:
SSH_USER: root
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_PORT: 22
DEPLOY_DIR: /root/my-nginx/html/www/mhaibaraai.cn
PM2_APP_NAME: mhaibaraai.cn
run: |
bash scripts/deploy.sh
- name: Deploy build artifacts to gh-pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./.output
publish_branch: gh-pages
force_orphan: true
commit_message: 'chore(release): build artifacts'
#!/usr/bin/env bash
set -euo pipefail
# Required environment variables
SSH_USER="${SSH_USER:?missing}"
SSH_HOST="${SSH_HOST:?missing}"
SSH_PORT="${SSH_PORT:?missing}"
DEPLOY_DIR="${DEPLOY_DIR:?missing}"
PM2_APP_NAME="${PM2_APP_NAME:-nuxt-app}"
# Validate environment variables
[[ "$SSH_PORT" =~ ^[0-9]+$ ]] && [[ "$SSH_PORT" -ge 1 ]] && [[ "$SSH_PORT" -le 65535 ]] || { echo "[deploy] Invalid SSH_PORT" >&2; exit 1; }
[[ "$DEPLOY_DIR" =~ \.\. ]] || [[ "$DEPLOY_DIR" == */ ]] && { echo "[deploy] Invalid DEPLOY_DIR" >&2; exit 1; }
[[ "$SSH_HOST" =~ ^[a-zA-Z0-9.-]+$ ]] || { echo "[deploy] Invalid SSH_HOST" >&2; exit 1; }
echo "[deploy] Host=${SSH_USER}@${SSH_HOST}:${SSH_PORT}"
echo "[deploy] Target dir=${DEPLOY_DIR}"
# Local safety checks
TARGET_DIR="${DEPLOY_DIR%/}/.output"
[[ -z "$TARGET_DIR" ]] || [[ "$TARGET_DIR" == "/" ]] || [[ "$(basename -- "$TARGET_DIR")" != ".output" ]] && { echo "[deploy] invalid TARGET_DIR='$TARGET_DIR'" >&2; exit 3; }
[[ ! -d ./.output ]] || [[ -z "$(ls -A ./.output 2>/dev/null || true)" ]] && { echo "[deploy] local ./.output missing or empty" >&2; exit 4; }
# Connection options with safe escaping
SSH_OPTS="-p $(printf '%q' "$SSH_PORT")"
RSYNC_SSH="ssh ${SSH_OPTS}"
SSH_TARGET="$(printf '%q' "$SSH_USER")@$(printf '%q' "$SSH_HOST")"
DEPLOY_DIR_ESC=$(printf '%q' "$DEPLOY_DIR")
# Sync files
RSYNC_PATH="mkdir -p ${DEPLOY_DIR_ESC}/.output && rsync"
rsync -az --delete-after -e "${RSYNC_SSH}" --rsync-path "${RSYNC_PATH}" ./.output/ "${SSH_TARGET}:${DEPLOY_DIR_ESC}/.output/"
[[ -f ./ecosystem.config.cjs ]] && rsync -az -e "${RSYNC_SSH}" ./ecosystem.config.cjs "${SSH_TARGET}:${DEPLOY_DIR_ESC}/ecosystem.config.cjs"
# Remote pm2 reload/start
ssh ${SSH_OPTS} "${SSH_TARGET}" "export DEPLOY_DIR='${DEPLOY_DIR}' PM2_APP_NAME='${PM2_APP_NAME}'; bash -s" <<'REMOTE_EOF'
set -eo pipefail
export NODE_OPTIONS=--max_old_space_size=1024
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"
# Resolve Node/npm global prefix bin
PREFIX_BIN=""
if command -v npm >/dev/null 2>&1; then
PREFIX_BIN="$(npm config get prefix 2>/dev/null)/bin"
elif [[ -d "$HOME/.local/share/fnm/node-versions" ]]; then
LATEST_NODE_DIR="$(ls -1dt "$HOME/.local/share/fnm/node-versions"/*/installation 2>/dev/null | head -n 1 || true)"
[[ -n "$LATEST_NODE_DIR" ]] && PREFIX_BIN="$LATEST_NODE_DIR/bin"
fi
[[ -z "$PREFIX_BIN" ]] && PREFIX_BIN="/usr/local/bin"
export PATH="$PREFIX_BIN:$PATH"
echo "[remote] prefix bin: $PREFIX_BIN"
# Locate pm2
PM2_BIN="$(command -v pm2 || true)"
[[ -z "$PM2_BIN" ]] && { echo "[remote] pm2 not found" >&2; exit 127; }
echo "[remote] using pm2: $PM2_BIN"
cd "${DEPLOY_DIR}"
mkdir -p .pm2
echo "[remote] PM2 operation for $PM2_APP_NAME..."
if "$PM2_BIN" describe "$PM2_APP_NAME" >/dev/null 2>&1; then
"$PM2_BIN" reload "$PM2_APP_NAME" --update-env
else
PM2_APP_NAME="$PM2_APP_NAME" "$PM2_BIN" start ecosystem.config.cjs
"$PM2_BIN" save
fi
"$PM2_BIN" status | cat
# Health check
if "$PM2_BIN" describe "$PM2_APP_NAME" | grep -q "status.*online"; then
echo "[remote] health check passed"
else
echo "[remote] health check failed" >&2
"$PM2_BIN" logs "$PM2_APP_NAME" --lines 20 --nostream || true
exit 1
fi
REMOTE_EOF
echo "[deploy] Completed"
package.json 增加 start 便于本地/服务调试:
{
"scripts": {
"start": "node .output/server/index.mjs"
}
}
首先确保站点地图已生成,并上传到 ./public/sitemap.xml 目录下。例如:https://mhaibaraai.cn/sitemap.xml

curl -I http://host.docker.internal:3000 应返回 200。pm2 logs 与 Nginx 访问/错误日志。better-sqlite3 ABI 不匹配,确保在目标机上重新安装与构建(见项目的 Nuxt 踩坑笔记)。