上周五下午三点,线上订单接口突然 502,排查一圈发现是新版本打包后漏传了 config.js —— 因为部署还是靠人工 scp + systemctl restart,手一抖,跳过了配置文件同步这步。
别让“手动上线”成为故障源头
很多小团队没上 Jenkins 或 GitLab CI,不是不想,是觉得太重。但真没必要。一个不到 50 行的 shell 脚本,配上 git hook 和简单权限控制,就能把「改完代码 → 推到 main → 自动上线」跑通。关键是:它能跑、好查、出错了立刻知道哪一步挂了。
一个真实可用的 deploy.sh 示例
假设你用的是 Node.js 服务,部署到 Ubuntu 服务器,代码托管在 GitHub,项目结构长这样:
my-app/
├── package.json
├── dist/
├── .env.production
└── deploy.sh本地开发完,运行 git push origin main 后,服务器自动拉取、安装依赖、重启服务。脚本核心逻辑如下:
#!/bin/bash
# deploy.sh - 放在项目根目录,chmod +x deploy.sh
APP_DIR="/var/www/my-app"
BACKUP_DIR="/var/www/backups"
LOG_FILE="/var/log/my-app-deploy.log"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
echo "[$(date)] 开始部署..." >> $LOG_FILE
# 进入项目目录
cd $APP_DIR || { echo "进入目录失败" >> $LOG_FILE; exit 1; }
# 备份当前 dist(可选)
mkdir -p $BACKUP_DIR
cp -r ./dist $BACKUP_DIR/dist-$TIMESTAMP >> $LOG_FILE 2>&1
# 拉取最新代码
git pull origin main >> $LOG_FILE 2>&1
if [ $? -ne 0 ]; then
echo "git pull 失败" >> $LOG_FILE
exit 1
fi
# 安装依赖(--production 跳过 dev 依赖)
npm ci --production >> $LOG_FILE 2>&1
if [ $? -ne 0 ]; then
echo "npm 安装失败" >> $LOG_FILE
exit 1
fi
# 构建(如需)
npm run build >> $LOG_FILE 2>&1
# 重启服务(假设用 pm2)
pm2 reload my-app --update-env >> $LOG_FILE 2>&1
if [ $? -ne 0 ]; then
echo "pm2 重启失败,尝试启动..." >> $LOG_FILE
pm2 start ecosystem.config.js --only my-app >> $LOG_FILE 2>&1
fi
echo "[$(date)] 部署完成" >> $LOG_FILE怎么让它“自动”起来?
把脚本放服务器上还不够,得让它感知到代码更新。最轻量的办法:在服务器上开个 webhook 接口(比如用 Python 的 Flask 写个三行路由),或者更简单——直接配个 git 的 post-receive hook。
比如,在服务器的 bare 仓库里(/var/repo/my-app.git/hooks/post-receive)加一行:
#!/bin/bash
GIT_WORK_TREE=/var/www/my-app git checkout -f main
cd /var/www/my-app && ./deploy.sh再给这个 hook 加执行权限:chmod +x post-receive。之后每次 push,服务器就自己动起来了。
几个踩过的坑,顺手记下来
• 环境变量别硬编码:.env 文件千万别提交到 git,用 scp .env.production user@server:/var/www/my-app/.env 单独传一次,脚本里只负责读取;
• 权限问题很常见:确保运行脚本的用户对 /var/www/my-app 有读写权限,pm2 也得用同一用户启;
• 日志别省:每一步都 echo + 重定向到日志,半夜报警时翻 log 比猜快十倍;
• 不要跳过测试:哪怕只是 curl -s http://localhost:3000/health | grep ok,上线后自动验一下接口通不通,比等用户反馈强。
脚本不追求多炫酷,能稳、能查、能快速回滚,就是好脚本。上线前多跑两遍,比写一百行文档都管用。