CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
部署 CodeRoadMap
这一课你将能做到
按规范目录落地 CodeRoadMap:/www/wwwroot/coderoadmap/ 放 JAR、前端产物与配置,属主/权限对齐运行用户
用 systemd 托管后端 :8080、PM2 托管前端 :3000,并让 Nginx 反代 :443,实现「首页 + /api/」全链路可访问
执行一次完整发布(换 JAR → restart → 健康检查 → 回滚演练),并把流程写成自己的检查单
为什么要学
前面十课全是零件:Shell 是语言、权限是规矩、systemd 是进程管家、SSH 是通道、防火墙是门。本课把它们组装成一台真正能访问的 CodeRoadMap ——这是本 track 的验收课:每一步都会回头用到之前学过的命令,缺课的地方会立刻暴露。
你要建立的工程认知是:「部署成功」= 目录 + 权限 + 进程 + 反代 + 端口策略同时正确 ,缺一个就是「半上线」。而「发布成功」= 上传 + 重启 + 验证 + 可回滚,缺回滚就不敢发布。
先建立图像
/www/wwwroot/coderoadmap/
├─ backend.jar ← Spring Boot 产物(systemd 托管)
├─ application-prod.yml ← 生产配置:DB 地址/密钥,属主 www、权限 640,绝不进 Git
├─ platform/ ← Next.js 构建产物(PM2 托管)
├─ releases/ ← 历史版本 JAR(回滚用)
├─ logs/ ← 应用日志
└─ scripts/ ─ 发布/回滚脚本
浏览器 ──HTTPS 443──► Nginx
├─ / → 127.0.0.1:3000 (PM2 → next start)
└─ /api/ → 127.0.0.1:8080 (systemd → java -jar) ──► 127.0.0.1:3306 MySQL
三个约定先说死:公网只见 Nginx (80/443)、业务端口只听 127.0.0.1 (8080/3000/3306)、进程全部有管家 (systemd/PM2,不是 nohup)。
跟着做
前置条件自查(前面各课的验收):
java -version && node -v # 包管理课:JDK17 + Node20 就位
systemctl is-active mysql # MySQL 活着
ssh prod "echo ok" # SSH 课:别名免密可连
sudo ufw status | grep -E "80|443" # 防火墙课:门面已开
步骤 1:目录与权限(文件课 + 用户课合体)
ssh prod
sudo mkdir -p /www/wwwroot/coderoadmap/{platform,logs,scripts,releases}
# 运行用户:宝塔默认 www;自建环境用你定义的服务用户(用户课)
sudo useradd -r -s /usr/sbin/nologin www 2>/dev/null || true
sudo chown -R www:www /www/wwwroot/coderoadmap
sudo chmod 750 /www/wwwroot/coderoadmap # 目录:属主全权,组可进,他人无份
# 双身份模型:deploy(人)+ www(服务)。deploy 维护经 sudo,www 跑服务
sudo usermod -aG www deploy 2>/dev/null || true
# 交叉验证(文件课的 sudo -u 验证法):
sudo -u www touch /www/wwwroot/coderoadmap/logs/.probe && echo "www 可写日志 ✅" \
&& sudo rm /www/wwwroot/coderoadmap/logs/.probe
步骤 2:上传产物(SSH 课的 scp/rsync)
本机执行(构建产物假设已在本地打好;构建应在 CI 完成,服务器不跑 npm build):
# 后端 JAR(Maven/Gradle 打出的 bootJar)
scp target/backend-1.0.0.jar prod:/tmp/backend.jar.new
# 前端 Next.js 构建产物(.next、public、package.json、next.config.js)
rsync -avz --exclude node_modules .next prod:/tmp/platform-new/
rsync -avz public package.json next.config.js prod:/tmp/platform-new/
ssh prod
# JAR:版本归档 + 软链切换(可回滚的关键设计)
sudo mv /tmp/backend.jar.new /www/wwwroot/coderoadmap/releases/backend-$(date +%Y%m%d%H%M).jar
sudo ln -sfn /www/wwwroot/coderoadmap/releases/backend-$(date +%Y%m%d%H%M).jar \
/www/wwwroot/coderoadmap/backend.jar
sudo chown www:www /www/wwwroot/coderoadmap/backend.jar
sudo chmod 644 /www/wwwroot/coderoadmap/backend.jar
# 前端:整体换目录(原子切换思路)
sudo rsync -a --delete /tmp/platform-new/ /www/wwwroot/coderoadmap/platform/
sudo chown -R www:www /www/wwwroot/coderoadmap/platform
sudo vim /www/wwwroot/coderoadmap/application-prod.yml
# 内容要点:datasource 指向 127.0.0.1:3306、server.address=127.0.0.1、密钥手动维护
sudo chown www:www /www/wwwroot/coderoadmap/application-prod.yml
sudo chmod 640 /www/wwwroot/coderoadmap/application-prod.yml # 组内只读,他人无权
步骤 3:后端 systemd 单元(进程课直接复用) sudo vim /etc/systemd/system/coderoadmap-backend.service
[Unit]
Description=CodeRoadMap Spring Boot backend
After=network.target mysql.service
[Service]
Type=simple
User=www
WorkingDirectory=/www/wwwroot/coderoadmap
ExecStart=/usr/bin/java -Xms512m -Xmx1024m -jar /www/wwwroot/coderoadmap/backend.jar \
--spring.profiles.active=prod \
--spring.config.additional-location=/www/wwwroot/coderoadmap/application-prod.yml
Restart=on-failure
RestartSec=5
SuccessExitStatus=143
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now coderoadmap-backend
sleep 15 && systemctl status coderoadmap-backend --no-pager
curl -s http://127.0.0.1:8080/actuator/health; echo # {"status":"UP"} 即成功
# 没起来看日志(命令课 + 进程课):
journalctl -u coderoadmap-backend -n 50 --no-pager
sudo mysql -e "CREATE DATABASE IF NOT EXISTS coderoadmap DEFAULT CHARACTER SET utf8mb4;"
sudo mysql -e "CREATE USER IF NOT EXISTS 'crm'@'127.0.0.1' IDENTIFIED BY '强密码自定';"
sudo mysql -e "GRANT ALL ON coderoadmap.* TO 'crm'@'127.0.0.1'; FLUSH PRIVILEGES;"
# 密码写进 application-prod.yml(640 权限保护),不要写进代码库
步骤 4:前端 PM2 托管(Node 生态的管家) sudo npm i -g pm2 # PM2 装一次即可
cd /www/wwwroot/coderoadmap/platform
npm ci --omit=dev # 只装运行依赖(构建已在 CI 完成)
pm2 start npm --name platform -- start -- -H 127.0.0.1 -p 3000
pm2 save # 保存进程列表
pm2 startup # 按提示执行它输出的 sudo env... 命令 → 开机自启
pm2 list # online 即成功
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3000/ # 200
pm2 logs platform --lines 20 --nostream # 排障看日志
步骤 5:Nginx 反代(门面) sudo vim /etc/nginx/sites-available/coderoadmap.conf
server {
listen 80;
server_name 你的域名;
client_max_body_size 50m;
# API 优先匹配,反代后端
location /api/ {
proxy_pass http://127.0.0.1:8080;
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;
}
# 其余全给前端
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
sudo ln -sfn /etc/nginx/sites-available/coderoadmap.conf /etc/nginx/sites-enabled/
sudo nginx -t # 语法检查,ok 才继续
sudo systemctl reload nginx # reload 而非 restart(不断连接)
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1/ # 本机验证反代通
# HTTPS(有域名后):certbot 自动改配置
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d 你的域名 # 按提示选 redirect;nginx -t && reload 自动完成
curl -I https://你的域名 | head -3 # HTTP/2 200
宝塔用户的差异:面板「添加站点 → 反代」生成的就是上面这类配置,理解了手写版,面板里的每个字段你都知道对应什么。
步骤 6:全链路验收 + 回滚演练 # 验收清单(从公网视角逐项过):
curl -s -o /dev/null -w "front:%{http_code}\n" https://你的域名/
curl -s https://你的域名/api/actuator/health; echo
sudo ss -tlnp | grep -E ':(80|443|3000|8080|3306)\s'
# 期望:80/443 → nginx(0.0.0.0);3000/8080/3306 → 127.0.0.1
# 故障演练:停前端,观察 502,再恢复(理解 Nginx 依赖上游)
pm2 stop platform && curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1/
pm2 start platform
# 回滚演练:releases 里挑上一个版本,切链重启(2 分钟内完成)
ls -lt /www/wwwroot/coderoadmap/releases/ | head -3
sudo ln -sfn /www/wwwroot/coderoadmap/releases/上一个版本.jar /www/wwwroot/coderoadmap/backend.jar
sudo systemctl restart coderoadmap-backend
curl -s http://127.0.0.1:8080/actuator/health; echo
步骤 7:把发布流程固化成脚本(scripts/ 的第一个住户) sudo vim /www/wwwroot/coderoadmap/scripts/release.sh
#!/bin/bash
# 用法:./release.sh /path/to/new.jar
set -euo pipefail
APP_DIR=/www/wwwroot/coderoadmap
STAMP=$(date +%Y%m%d%H%M)
test -f "$1" || { echo "用法: $0 新JAR路径"; exit 1; }
# systemctl 走 sudoers 白名单(用户课的 NOPASSWD 配置)
sudo mv "$1" "$APP_DIR/releases/backend-$STAMP.jar"
sudo ln -sfn "$APP_DIR/releases/backend-$STAMP.jar" "$APP_DIR/backend.jar"
sudo chown www:www "$APP_DIR/backend.jar"
sudo systemctl restart coderoadmap-backend
sleep 12
curl -sf http://127.0.0.1:8080/actuator/health > /dev/null \
&& echo "RELEASE OK: $STAMP" \
|| { echo "HEALTH CHECK FAILED → 自动回滚"; \
sudo ln -sfn "$(ls -t $APP_DIR/releases/*.jar | sed -n 2p)" "$APP_DIR/backend.jar"; \
sudo systemctl restart coderoadmap-backend; exit 1; }
chmod +x /www/wwwroot/coderoadmap/scripts/release.sh
健康检查不过自动回滚——这一段脚本就是「敢在周五下午发布」的全部底气。CI 集成(GitHub Actions scp + ssh 执行此脚本)见 github-actions 轨道。
讲清楚要点
因为多进程协作,所以部署是「五件事的与运算」 :目录对、权限对、进程对、反代对、端口策略对。逐层验收(本机 curl → 127.0.0.1 → 公网),任何一层断了先修那一层。
因为回滚比修复快,所以发布必须留后路 :releases/ 归档 + 软链切换 + 健康检查不过自动回滚。磁盘允许时保留最近 5 个版本,配 cron 清理更早的。
因为配置含密钥,所以 prod 配置与代码库分离 :服务器落盘、640 权限、不进 Git。CI 只传产物不传密钥——这是安全底线不是风格问题。
因为端口唯一,所以「覆盖文件」不等于「发布新版本」 :JVM 加载的是启动时的 JAR,改完必须 restart。发布流程 = 停旧(或直接 restart)→ 换文件 → 起新 → 验活,管家(systemd/PM2)负责保证这四步之后进程活着。
因为前端是构建产物,所以服务器只装运行时 :npm ci --omit=dev 而不是全量 install,绝不在生产机上 build(慢、不可复现、内存吃紧)。构建进 CI(github-actions 轨道)。
因为公网只应有 Nginx,所以应用全部绑 127.0.0.1 :HTTPS/限流/缓存/日志都收口在 Nginx 一层,Java/Node 不直接面对公网——这是性能、安全、证书管理的三重需要。
你可能会懵的地方
宝塔「网站根目录」应该填 platform 还是加反代?
Next.js 动态渲染必须反代到 :3000(PM2 跑着的服务),根目录方式只适合纯静态导出(output: 'export')。判断标准:访问 / 时浏览器 Network 里是否有 /_next/... 动态请求。有 → 反代;无 → 静态目录也行。
curl 127.0.0.1:8080 通,但经域名访问 /api/ 404 或 502?
404:Nginx 的 location /api/ 匹配后 proxy_pass 是否带了路径拼接问题(带斜杠=替换前缀);应用自身 context-path 是否为空。502:上游没起或 Nginx 的目标写错。区分工具:nginx error.log 的 tail + nginx -T | grep -A3 location。
actuator/health 返回 404?
Spring Boot 2.x 需要暴露端点:management.endpoints.web.exposure.include=health;或你们项目有统一前缀(/api/actuator/health)。以实际项目配置为准,把「健康 URL」写进发布脚本。
CI 显示 deploy 成功,浏览器还是旧页面?
四连查:scp/rsync 目标路径对吗(ls -lt releases/)?restart 执行了吗(systemctl status 看 ActiveEnterTimestamp)?多台机器发的是同一台吗(hostname)?浏览器/CDN 缓存吗(Ctrl+Shift+R 或 curl 带 -H "Cache-Control: no-cache")。
服务器内存 2G,MySQL + JAR + Node 同时跑得动吗?
紧张但可行:MySQL 调小 buffer pool(~256M)、JVM -Xmx768m、Node 本身不大;再加 2G swap 兜底(fallocate -l 2G /swapfile 系列)。生产正式跑建议 4G 起步。预算方法见进程课的内存三口径。
错法对照 错法 问题 正解 公网直连 8080 当「已上线」 无 TLS/限流/缓存,应用裸奔 Nginx 443 对外,应用绑 127.0.0.1 生产配置提交进 Git DB 密码等秘钥永久留在提交历史 服务器 640 落盘,CI 不碰密钥 覆盖 JAR 不重启就验收 跑的还是旧字节码 restart 是发布的组成部分,验活后才算完成 目录属主 root、进程用户 www Permission denied 连环报 chown 对齐运行用户(文件课标准流程) 生产机 npm install/build 当常规流程 慢、不可复现、内存爆 CI 构建 + rsync 产物 + npm ci --omit=dev 出错后反复 restart「碰运气」 日志刷屏,根因永远看不见 journalctl/应用日志先定性,再动手 发布不留旧版本 回滚只能重新打包,故障窗口拉长 releases/ 归档 + 软链切换,秒级回滚
卡点急救
后端起不来:先分层看 ——systemctl status 看退出码;journalctl -u coderoadmap-backend -n 80 看临终日志。高频根因:① DB 连不上(MySQL 没建库/账号错/密码错)→ 补步骤 3 的建库授权;② 端口被占 → ss -tlnp | grep 8080;③ yml 缩进/字段错 → 本地先跑一遍同配置。
前端 502 且 pm2 list 显示 errored :pm2 logs platform --lines 50 --nostream 看崩溃原因。高频:.next 不完整(rsync 漏文件)、npm ci 没跑(模块缺失)、端口被旧进程占。修复后 pm2 save 固化。
HTTPS 证书申请失败 :域名未解析到本机(dig +short 你的域名 核对)、80 未放行(certbot 校验走 HTTP)、Nginx server_name 与域名不一致。三项都过再重跑 certbot。
静态资源 404(页面能开但 CSS/JS 丢) :多为产物目录不完整或 Nginx 没把 /_next/ 反代给 3000。本课配置里 location / 已覆盖;若拆了静态规则,检查 root/alias 指向与实际目录。
回滚后「还是新版」 :软链切了但没 restart(JVM 不热加载);或 systemd 的 ExecStart 写的是绝对路径文件而非软链——确认 ExecStart 指向 /www/wwwroot/coderoadmap/backend.jar(软链本身),daemon-reload 后 restart。
面试常问
描述你部署一个 Spring Boot + Next.js 项目的完整流程。
主线:① 规范目录 + 用户/权限模型(www 运行、deploy 操作);② CI 构建产物,scp/rsync 上传,releases 归档 + 软链切换;③ systemd 托管 JAR(开机自启/崩溃重启),PM2 托管前端;④ Nginx 反代 80/443,应用全部绑 127.0.0.1,MySQL 仅回环;⑤ 发布脚本带健康检查与自动回滚;⑥ 验收从公网逐层 curl。每一步都对应 Linux 层的可验证命令。
应用健康检查你们怎么做?部署脚本如何利用它?
进程层:systemd 只保证进程活;业务层用 actuator/health(DB/Redis 检查包含在内)。发布脚本 curl -sf health || 自动回滚到上一版本;外部监控定时打健康接口,失败告警。「假活」(进程在但接口 5xx)靠业务健康检查兜住,这是 systemctl status 看不出来的。
线上发布后出现 5xx,你的应急流程?
决策树:健康检查挂 → 直接回滚(脚本已备);个别接口 5xx → 看错误日志(journalctl + 应用 logs)定位是新代码问题还是环境差异;流量突增导致 → 先限流/扩容。原则:先止血(回滚/限流)再查因 ,把 MTTR 压到最短。
为什么生产环境不让应用直接对外提供服务,而要经 Nginx 反代?
Nginx 统一收口:TLS 终结(证书一处管理)、HTTP/2、gzip/缓存、限流防刷、静态资源直接服务、多 upstream 负载与灰度切换;应用进程重启时 Nginx 可缓存 502 页面且端口稳定不漂移。安全上公网只暴露 80/443,应用绑定回环,攻击面最小化。
如何保证「回滚」是随时可用的?
三件套:版本归档(releases/ 保留 N 个历史 JAR/产物)、切换机制(软链/PM2/镜像 tag,切换动作≤1 条命令)、切换后的验证自动化(健康检查脚本)。定期演练回滚(本课步骤 6),没演练过的回滚等于没有回滚。
自测三问
「部署成功」由哪五层条件构成?写出每层对应的验证命令。
发布新 JAR 的完整命令序列是什么?如果健康检查失败,你如何 1 分钟内回到上一个版本?
application-prod.yml 为什么权限是 640 而不是 644?它应该由谁拥有、谁能读?
小结一句话 CodeRoadMap 的部署拓扑一句话:产物进 /www/wwwroot(属主对齐运行用户),JAR 交 systemd、前端交 PM2、公网只留 Nginx 443、数据库锁回环;发布 = 归档 + 软链 + restart + 健康检查 + 可回滚 ——至此,你已能独立完成一次可敬畏的生产部署。
延伸