
全栈项目部署实战指南:Java / Python / Vue / React 一站式搞定
全栈项目部署实战指南:Java / Python / Vue / React 一站式搞定
从本地开发到生产上线,覆盖 Spring Boot、FastAPI/Flask、Vue、React 四大技术栈的完整部署方案,结合 Docker 容器化 + Nginx 反向代理 + CI/CD 自动化,帮你打通项目上线的"最后一公里"。
本文中的所有部署方式,均来自我日常工作中维护的多个生产项目,以及个人开源项目的实战经验。如果对你有帮助,希望给我的开源项目点赞支持一下吧,你们的点赞和收藏都是我的动力! https://github.com/DevYangJC
🧪 实战推荐: 如果你想找一个前后端分离的真实项目来练手部署,推荐试试我的开源项目 —— DataLoom,把在线表格(类似 Excel)像零件一样装进你的项目里。它的架构和本文完全对应:前端是独立 SPA(Vue 3 + Luckysheet)、后端是独立微服务(Spring Boot)、前后端之间只有 REST API 通信。读完本文后,用它来实操一遍"前端 Nginx 托管 + 后端脚本部署 + Nginx 反向代理"的完整流程,体会会更深刻。
目录
- 一、部署前置:服务器环境准备
- 二、Java(Spring Boot)项目部署
- 三、Python(FastAPI / Flask)项目部署
- 四、前端(Vue / React)项目部署
- 五、Nginx 反向代理与域名配置
- 六、Docker Compose 一键编排
- 七、CI/CD 自动化部署
- 八、生产环境最佳实践
- 九、常见问题排查

一、部署前置:服务器环境准备
1.1 服务器选型建议
| 场景 | 推荐配置 | 适用项目 |
|---|---|---|
| 个人项目/测试 | 2C4G | 单体应用、静态前端 |
| 中小型生产 | 4C8G | Spring Boot + 前端 |
| 中大型生产 | 8C16G+ | 微服务、多容器编排 |
云服务商选择:国内推荐阿里云、腾讯云、华为云;海外推荐 AWS、DigitalOcean。学生和开发者可关注各厂商的优惠活动,通常首年价格极低。
1.2 基础环境安装
在开始安装之前,先想清楚一个问题:你部署的项目用到了哪些技术? 不同的项目需要不同的运行环境,没必要一股脑全装上。下面这张表帮你快速判断:
| 你要部署什么 | 必装环境 | 可选环境 |
|---|---|---|
| Spring Boot 项目 | JDK 17+ | Maven(服务器构建时需要) |
| FastAPI / Flask 项目 | Python 3.10+、pip | Nginx(生产环境反向代理) |
| Vue / React 前端项目 | Node.js 20+(构建时需要) | Nginx(托管静态文件) |
| 使用 Docker 部署 | Docker、Docker Compose | — |
| 需要数据库 | MySQL / PostgreSQL | Redis(缓存) |
安装顺序建议:先装基础工具(curl、git 等)→ 再装运行环境(JDK、Python、Node)→ 最后装基础设施(Docker、Nginx、数据库)。因为后面的软件可能依赖前面的工具来下载和安装。
以 CentOS 7/8 为例,下面按步骤安装:
▼bash复制代码# ======================================== # 第一步:系统更新(装任何东西之前先更新) # ======================================== # CentOS 7 sudo yum update -y # CentOS 8 / Rocky Linux / AlmaLinux sudo dnf update -y # ======================================== # 第二步:基础工具(必须装,后面都会用到) # ======================================== sudo yum install -y curl wget git vim unzip net-tools lsof # curl - 下载文件用(装 Docker、Node 都靠它) # wget - 另一个下载工具,某些脚本只支持 wget # git - 拉代码用 # vim - 编辑配置文件 # unzip - 解压 zip 包 # net-tools - netstat 等网络排查工具 # lsof - 查看端口占用(服务启动失败时排查用) # ======================================== # 第三步:Docker 和 Docker Compose # ======================================== # 安装 Docker curl -fsSL https://get.docker.com | sh sudo systemctl start docker sudo systemctl enable docker # 把当前用户加入 docker 组,这样不用每次都 sudo sudo usermod -aG docker $USER # ⚠️ 执行上面这条后需要重新登录 SSH 才能生效 # 安装 Docker Compose sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" \ -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose # 验证 docker --version docker-compose --version # ======================================== # 第四步:Nginx(前端托管 + 反向代理) # ======================================== # CentOS 必须先装 epel-release 源,不然找不到 nginx 包 sudo yum install -y epel-release sudo yum install -y nginx sudo systemctl start nginx sudo systemctl enable nginx # ======================================== # 第五步:JDK 17(Java 项目必装) # ======================================== sudo yum install -y java-17-openjdk java-17-openjdk-devel # 验证 java -version # javac -version # 验证开发工具包(服务器构建时需要) # 如果你的项目还在用 JDK 8 或 11,装对应版本: # sudo yum install -y java-11-openjdk # JDK 11 # sudo yum install -y java-1.8.0-openjdk # JDK 8 # 多版本 JDK 共存时,用 alternatives 切换默认版本: # sudo alternatives --config java # ======================================== # 第六步:Python 3(FastAPI / Flask 项目必装) # ======================================== # CentOS 7(系统自带 Python 2,需要额外装 Python 3) sudo yum install -y https://repo.ius.io/ius-release-el7.rpm sudo yum install -y python3u python3u-pip python3u-venv # CentOS 8+ 自带 Python 3 sudo dnf install -y python3 python3-pip # 验证 python3 --version pip3 --version # 设置 pip 国内镜像(加速下载,默认源在国外很慢) pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple # ======================================== # 第七步:Node.js 20+(前端构建时需要) # ======================================== # 注意:Node.js 只在"服务器上直接构建"时才需要 # 如果你在本地构建好 dist/ 再上传服务器,可以不装 curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash - sudo yum install -y nodejs # 验证 node --version npm --version # 设置 npm 国内镜像(加速下载) npm config set registry https://registry.npmmirror.com # ======================================== # 第八步:Git(如果需要在服务器上拉代码) # ======================================== sudo yum install -y git # 配置 Git 用户信息(首次使用需要) git config --global user.name "你的名字" git config --global user.email "your@email.com" # 如果需要从私有仓库拉代码,配置 SSH 密钥: # ssh-keygen -t ed25519 -C "your@email.com" # 然后把 ~/.ssh/id_ed25519.pub 的内容添加到 Git 平台的 SSH Keys 设置中
安装后验证清单:装完跑一遍,确保每个工具都正常工作。如果某个命令报
command not found,说明没装上或者环境变量没配好:▼bash复制代码java -version # ✅ 应该输出 openjdk version "17.x.x" python3 --version # ✅ 应该输出 Python 3.x.x node --version # ✅ 应该输出 v20.x.x docker --version # ✅ 应该输出 Docker 2x.x.x nginx -v # ✅ 应该输出 nginx version: nginx/1.x.x git --version # ✅ 应该输出 git version 2.x.x
1.3 部署前检查清单
很多新手拿到一台服务器就急着装软件、传代码、启动服务,结果到处报错——端口被占了、数据库连不上、文件权限不对、配置文件少写了逗号……这些问题如果在部署前花 10 分钟检查一遍,能省下好几个小时的排错时间。
部署前的检查就像出门旅行前的"伸手要钱"(身份证、手机、钥匙、钱包)——看似啰嗦,但少一样都走不了。
▼mermaid复制代码graph TD A["📋 部署前检查"] --> B["1️⃣ 代码与版本"] A --> C["2️⃣ 配置与依赖"] A --> D["3️⃣ 权限与密钥"] A --> E["4️⃣ 网络与端口"] A --> F["5️⃣ 磁盘与资源"] B --> G["✅ 全部通过 → 开始部署"] C --> G D --> G E --> G F --> G B -.->|"❌ 不通过"| H["🔧 修复后重新检查"] C -.->|"❌ 不通过"| H D -.->|"❌ 不通过"| H E -.->|"❌ 不通过"| H F -.->|"❌ 不通过"| H H --> A
1️⃣ 代码与版本检查
| 检查项 | 怎么检查 | 为什么重要 |
|---|---|---|
| 代码是否已提交并推送到远程 | git status 看有没有未提交的改动 | 你部署的应该是远程仓库的代码,而不是本地改了一半的版本 |
| 部署的版本号是否正确 | git tag 或 git log --oneline -5 确认当前版本 | 避免部署了错误的分支或旧的提交 |
| 分支是否正确 | git branch 确认是 release/main 分支 | 部署了开发分支的代码上生产,后果你懂的 |
| JAR 包 / 构建产物是否最新 | 对比构建时间和代码提交时间 | 避免用了缓存的旧包,新代码没打进去 |
▼bash复制代码# 在本地或 CI 环境中执行 git status # 确认没有未提交的改动 git log --oneline -5 # 确认最新提交是你期望的版本 git tag -l "v*" # 查看所有版本标签 # 打一个版本标签(发布时建议打 tag) git tag -a v1.2.0 -m "发布 v1.2.0" git push origin v1.2.0
2️⃣ 配置与依赖检查
| 检查项 | 怎么检查 | 为什么重要 |
|---|---|---|
| 配置文件是否齐全 | 确认 application-prod.yml / .env 等文件存在 | 缺了配置文件,服务启动后连不上数据库、读不到参数 |
| 数据库连接信息是否正确 | 检查 host、port、用户名、数据库名 | 配置写错了或者指向了测试库,生产数据就乱套了 |
| 第三方服务密钥是否配置 | 检查短信、支付、OSS 等的 API Key | 缺了密钥,相关功能全部不可用 |
| 依赖版本是否匹配 | 对比开发环境和生产环境的 JDK/Python/Node 版本 | "在我电脑上能跑"最常见的原因就是版本不一致 |
| 环境变量是否设置 | 检查 .env 文件或 export 的变量 | 密码、密钥等敏感配置通常通过环境变量注入 |
▼bash复制代码# 检查配置文件是否存在且内容完整 ls -la /opt/app/user-service/config/ cat /opt/app/user-service/config/application-prod.yml # 检查关键配置项 grep -E "datasource|redis|port" /opt/app/user-service/config/application-prod.yml # 测试数据库是否可连接(在服务器上执行) mysql -h 10.0.0.100 -u appuser -p -e "SELECT 1" # MySQL # python3 -c "import redis; r=redis.Redis(host='10.0.0.100',port=6379,password='xxx'); print(r.ping())" # Redis # 检查 Python 依赖是否安装 pip3 list | grep -E "fastapi|flask|gunicorn|uvicorn" # 检查 Node.js 依赖是否安装(前端项目) ls node_modules/ | head # 确认依赖已安装
3️⃣ 权限与密钥检查
| 检查项 | 怎么检查 | 为什么重要 |
|---|---|---|
| SSH 密钥是否配置 | ssh -T git@github.com 测试连通性 | 没有密钥就没法从私有仓库拉代码 |
| 文件目录权限是否正确 | ls -la /opt/app/ 查看权限 | 权限不对会导致应用无法读取配置或写入日志 |
| 应用是否以非 root 用户运行 | 检查启动脚本中的用户身份 | root 运行应用一旦被攻破,整个服务器都沦陷 |
| SSL 证书是否有效 | openssl x509 -enddate 检查过期时间 | 证书过期用户访问会提示"不安全",直接流失用户 |
| 数据库用户权限是否最小化 | 检查应用用的数据库账号是否只有必要的权限 | 给应用 root 权限 = 给小偷配了万能钥匙 |
▼bash复制代码# ---- SSH 密钥检查 ---- # 测试 GitHub SSH 连通性 ssh -T git@github.com # ✅ 看到 "Hi xxx! You've successfully authenticated" 说明密钥配置正确 # 生成新的 SSH 密钥(如果没有的话) ssh-keygen -t ed25519 -C "deploy@myapp" # 把公钥添加到 Git 平台:cat ~/.ssh/id_ed25519.pub 复制内容 # ---- 文件权限检查 ---- # 应用目录权限:所有者可读写执行,组和其他用户只读 ls -la /opt/app/user-service/ # ✅ 正确:drwxr-xr-x appuser appgroup ... # ❌ 危险:drwxrwxrwx (777,任何人都能改) # 修正权限 chown -R appuser:appgroup /opt/app/user-service/ chmod 755 /opt/app/user-service/bin/ chmod 600 /opt/app/user-service/config/application-prod.yml # 配置文件只有所有者可读写 # ---- SSL 证书有效期检查 ---- # Let's Encrypt 证书有效期 90 天,务必设置自动续期 openssl x509 -enddate -noout -in /etc/letsencrypt/live/example.com/fullchain.pem # 检查自动续期定时任务 sudo crontab -l | grep certbot # 如果没有,添加一个: # echo "0 3 * * * certbot renew --quiet" | sudo crontab -
4️⃣ 网络与端口检查
| 检查项 | 怎么检查 | 为什么重要 |
|---|---|---|
| 应用端口是否被占用 | lsof -i :8080 或 netstat -tlnp | grep 8080 | 端口被别的程序占了,你的服务启动就报 "Address already in use" |
| 服务器能否访问外部网络 | curl -I https://www.baidu.com | 服务器出不了外网,npm install / pip install 就会失败 |
| 数据库服务器是否可达 | telnet 10.0.0.100 3306 或 nc -zv 10.0.0.100 3306 | 连不上数据库,应用启动就报错 |
| 防火墙是否放通了必要端口 | sudo firewall-cmd --list-ports | 端口没开,外部请求根本进不来 |
| DNS 解析是否正常 | nslookup your-domain.com 或 dig your-domain.com | 域名没解析到服务器 IP,用户访问不了 |
▼bash复制代码# 检查端口占用(部署前务必检查!) lsof -i :8080 # 检查 8080 端口 lsof -i :80 # 检查 80 端口 netstat -tlnp | grep -E "8080|80|443|3306" # 如果端口被占用,找到并停掉占用的进程 kill -15 <PID> # 先礼貌退出 # kill -9 <PID> # 实在不停再强制杀 # 测试服务器网络连通性 curl -I https://www.baidu.com # 测试外网 ping 10.0.0.100 # 测试内网数据库服务器 nc -zv 10.0.0.100 3306 # 测试数据库端口是否可连 nc -zv 10.0.0.100 6379 # 测试 Redis 端口是否可连 # DNS 解析检查 nslookup your-domain.com # 如果域名还没解析,可以先在本地 hosts 文件中临时配置: # echo "你的服务器IP your-domain.com" | sudo tee -a /etc/hosts
5️⃣ 磁盘与资源检查
| 检查项 | 怎么检查 | 为什么重要 |
|---|---|---|
| 磁盘剩余空间 | df -h | 磁盘满了日志写不进去、数据库崩溃,问题一个比一个大 |
| 内存是否充足 | free -h | 内存不够 Java 直接 OOM 崩掉 |
| CPU 负载是否正常 | top -bn1 | head -5 | CPU 已经跑满了再加服务,新服务也起不来 |
| 日志清理策略是否设置 | du -sh /var/log/ 检查日志大小 | 日志无限增长是磁盘爆满的头号原因 |
▼bash复制代码# 磁盘空间(使用率超过 80% 就要警惕了) df -h # Filesystem Size Used Avail Use% Mounted on # /dev/vda1 40G 15G 23G 40% / ← ✅ 正常 # /dev/vda1 40G 35G 3G 93% / ← ⚠️ 危险!赶紧清理 # 内存(关注 available 列,这是实际可用内存) free -h # total used free available # Mem: 7.6G 3.2G 1.1G 4.0G ← ✅ 充裕 # Mem: 7.6G 6.8G 128M 500M ← ⚠️ 偏紧,考虑加内存或减少 JVM 分配 # CPU 负载(load average 三个数分别代表 1/5/15 分钟的平均负载) # 数值超过 CPU 核心数说明过载了 top -bn1 | head -5 # 大文件排查(找出占空间最多的目录) du -sh /* | sort -rh | head -10
一键检查脚本:如果你觉得上面逐项检查太麻烦,可以把所有检查项写成一个脚本,部署前跑一次就行。把下面的脚本保存为
pre-deploy-check.sh,每次部署前执行bash pre-deploy-check.sh:▼bash复制代码#!/bin/bash # 部署前一键检查脚本 # 用法: bash pre-deploy-check.sh PASS=0; FAIL=0 check() { local desc="$1" cmd="$2" echo -n "[$(echo $desc | cut -c1-20)] ... " if eval "$cmd" &>/dev/null; then echo "✅ 通过"; ((PASS++)) else echo "❌ 未通过"; ((FAIL++)) fi } echo "========== 部署前检查 ==========" echo "" echo "--- 环境 ---" check "Java 安装" "java -version" check "Python 安装" "python3 --version" check "Node.js 安装" "node --version" check "Docker 安装" "docker --version" check "Nginx 安装" "nginx -v" check "Git 安装" "git --version" echo "" echo "--- 网络 ---" check "外网连通" "curl -sI https://www.baidu.com" check "DNS 解析" "nslookup baidu.com" echo "" echo "--- 资源 ---" check "磁盘 > 20%" "test $(df / | tail -1 | awk '{print $5}' | tr -d '%') -lt 80" check "内存 > 500M" "test $(free -m | awk '/Mem/{print $7}') -gt 500" echo "" echo "========== 结果: ✅ ${PASS} 通过, ❌ ${FAIL} 未通过 ==========" if [ $FAIL -gt 0 ]; then echo "⚠️ 请先修复未通过的项目再进行部署!" exit 1 fi
1.4 防火墙与安全组
▼bash复制代码# CentOS 7/8 使用 firewalld sudo systemctl start firewalld sudo systemctl enable firewalld # 开放常用端口 sudo firewall-cmd --permanent --add-port=22/tcp # SSH sudo firewall-cmd --permanent --add-port=80/tcp # HTTP sudo firewall-cmd --permanent --add-port=443/tcp # HTTPS sudo firewall-cmd --permanent --add-port=8080/tcp # Spring Boot 默认端口 # sudo firewall-cmd --permanent --add-port=3306/tcp # MySQL(仅内网访问,不建议公网开放) # 重载防火墙使规则生效 sudo firewall-cmd --reload # 查看已开放端口 sudo firewall-cmd --list-ports
安全提醒:数据库端口(3306、5432、6379 等)务必不要直接暴露在公网,应通过安全组限制为内网访问。
二、Java(Spring Boot)项目部署

2.1 项目打包
Spring Boot 项目内嵌 Tomcat,直接打成可执行 JAR 即可:
▼bash复制代码# Maven 项目 mvn clean package -DskipTests # Gradle 项目 gradle clean build -x test
构建产物在 target/ 目录下,类似 user-service-1.0.0.jar。
2.2 方式一:脚本化部署(推荐,多服务运维首选)
为什么要用脚本部署?
你可能想问:Java 项目直接一条 java -jar xxx.jar 不就能跑了吗?确实能跑,但问题来了——你的服务器重启了怎么办?服务挂了怎么办?你要更新 JAR 包怎么办?每次都手动敲命令,时间长了自己都忘了怎么操作,换个人来接手更是两眼一抹黑。
脚本化部署就是把日常操作"录"下来,变成一条命令就能搞定的事情。就像微波炉的"一键热牛奶"按钮,你不需要知道微波炉内部怎么运作,按一下就行。同样的道理:
- 服务没启动,运行脚本 → 自动启动
- 服务已经在跑,运行同一个脚本 → 自动重启(先停再启)
- 要更新版本,运行备份脚本 → 旧版本安全保存,出问题随时回退
这种方式最大的好处就是简单可靠——不需要学 Docker、不需要懂容器网络、不需要额外的运维工具,一台装了 Java 的 Linux 服务器就能跑。对于大多数中小项目来说,这就够了。
▼mermaid复制代码graph TD A["运行 ./restart.sh"] --> B{"服务是否在运行?"} B -->|没在跑| C["启动服务<br/>nohup java -jar xxx.jar &"] B -->|已经在跑| D["先停掉旧进程<br/>kill 旧PID"] D --> E["等待进程完全退出"] E --> C C --> F["记录新PID<br/>写入 pid 文件"] F --> G["✅ 服务已就绪"]
目录结构设计
在开始之前,先说清楚文件放在哪里。一个好的目录结构就像一个整理好的工具箱——扳手在哪、螺丝刀在哪,一目了然,不用每次都翻箱倒柜。
我们规定所有服务都放在 /opt/app/ 下面,每个服务一个独立的文件夹,里面的子目录各有各的用处:
▼text复制代码/opt/app/ ├── user-service/ # 用户服务 │ ├── app/ │ │ └── user-service.jar # JAR 包(应用本体) │ ├── config/ │ │ └── application-prod.yml # 外部配置文件 │ ├── logs/ # 运行日志 │ ├── tmp/ # 临时文件(存放 PID 文件等) │ └── bin/ │ ├── restart.sh # 重启/启动脚本(核心,就这一个) │ └── backup.sh # 备份脚本 │ ├── api-gateway/ # API 网关 │ ├── app/ │ │ └── api-gateway.jar │ ├── config/ │ │ └── application-prod.yml │ ├── logs/ │ ├── tmp/ │ └── bin/ │ ├── restart.sh │ └── backup.sh │ └── backup/ # 发布备份(回滚用) ├── user-service/ └── api-gateway/
这些目录都是什么用?用大白话给你解释一下:
| 目录 | 里面放什么 | 为什么要单独放 |
|---|---|---|
app/ | JAR 包,你的应用本体 | 和配置文件分开,更新时只需替换这里面的 JAR 包,配置不受影响 |
config/ | application-prod.yml 外部配置 | 改数据库密码、调端口号,直接编辑这个文件就行,不用重新打包 |
logs/ | 运行产生的日志文件 | 日志会越来越大,单独放方便定期清理,不会撑爆磁盘 |
tmp/ | PID 文件(记录进程号) | 脚本靠这个文件判断服务是否在运行,就像门上挂的"有人/无人"牌子 |
bin/ | 管理脚本 | 所有运维操作都在这里,一条命令搞定 |
backup/ | 旧版本 JAR 包备份 | 更新前自动备份,万一新版本有问题,把旧的换回来就行 |
一键初始化目录结构
每新增一个服务都要手动建这么多文件夹?当然不用。下面这个初始化脚本帮你一键创建好所有目录,新项目部署时跑一次就行:
▼bash复制代码#!/bin/bash #=================================================== # 初始化服务目录结构 # 用法: ./init-structure.sh <服务名> # 示例: ./init-structure.sh user-service #=================================================== APP_HOME="/opt/app" SVC_NAME=$1 # 检查参数 if [ -z "$SVC_NAME" ]; then echo "❌ 请提供服务名称!" echo "用法: $0 <服务名>" echo "示例: $0 user-service" exit 1 fi SVC_DIR="$APP_HOME/$SVC_NAME" # 检查是否已存在 if [ -d "$SVC_DIR" ]; then echo "⚠️ 目录已存在: $SVC_DIR" read -p "是否继续?(会跳过已存在的目录)[y/N] " confirm if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then echo "已取消" exit 0 fi fi echo "🔧 正在初始化服务目录: $SVC_DIR" # 创建各子目录 mkdir -p "$SVC_DIR/app" mkdir -p "$SVC_DIR/config" mkdir -p "$SVC_DIR/logs" mkdir -p "$SVC_DIR/tmp" mkdir -p "$SVC_DIR/bin" mkdir -p "$APP_HOME/backup/$SVC_NAME" # 创建重启脚本 cat > "$SVC_DIR/bin/restart.sh" << 'SCRIPT' #!/bin/bash #=================================================== # 服务重启/启动脚本 # 逻辑:服务没启动 → 启动;已经在跑 → 重启 # 用法: ./restart.sh [prod|dev] #=================================================== # ---- 基础配置(按实际项目修改这三行)---- APP_NAME="这里改成你的服务名" JAR_NAME="这里改成你的jar包名.jar" ACTIVE_PROFILE=${1:-prod} # ---- 以下不用改 ---- APP_DIR=$(cd "$(dirname "$0")/.." && pwd) JAR_PATH="$APP_DIR/app/$JAR_NAME" CONFIG_DIR="$APP_DIR/config" LOG_DIR="$APP_DIR/logs" PID_FILE="$APP_DIR/tmp/app.pid" # JVM 参数 JVM_OPTS="-Xms512m -Xmx1024m" JVM_OPTS="$JVM_OPTS -XX:+UseG1GC" JVM_OPTS="$JVM_OPTS -XX:+HeapDumpOnOutOfMemoryError" JVM_OPTS="$JVM_OPTS -XX:HeapDumpPath=$LOG_DIR/heapdump.hprof" JVM_OPTS="$JVM_OPTS -Djava.security.egd=file:/dev/./urandom" JVM_OPTS="$JVM_OPTS -Dfile.encoding=UTF-8" # 颜色输出 RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' info() { echo -e "${GREEN}[INFO]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; } # 获取 PID get_pid() { if [ -f "$PID_FILE" ]; then cat "$PID_FILE" 2>/dev/null fi } # 检查是否在运行 is_running() { local pid=$(get_pid) [ -z "$pid" ] && return 1 if ps -p "$pid" > /dev/null 2>&1; then return 0 else rm -f "$PID_FILE" return 1 fi } # 停止服务 do_stop() { if ! is_running; then warn "$APP_NAME 未在运行" return 0 fi local pid=$(get_pid) info "正在停止 $APP_NAME (PID: $pid) ..." # 先发 SIGTERM,让应用优雅关闭(保存数据、释放连接) kill -15 "$pid" # 等最多 30 秒 for i in $(seq 1 30); do if ! ps -p "$pid" > /dev/null 2>&1; then info "$APP_NAME 已优雅停止" rm -f "$PID_FILE" return 0 fi sleep 1 done # 超时了还没停,强制杀掉 warn "优雅关闭超时,强制终止 ..." kill -9 "$pid" rm -f "$PID_FILE" info "$APP_NAME 已强制停止" } # 启动服务 do_start() { if is_running; then warn "$APP_NAME 已在运行 (PID: $(get_pid)),将重启 ..." do_stop sleep 2 fi if [ ! -f "$JAR_PATH" ]; then error "找不到 JAR 文件: $JAR_PATH" exit 1 fi mkdir -p "$LOG_DIR" "$APP_DIR/tmp" info "正在启动 $APP_NAME ..." info " 环境 : $ACTIVE_PROFILE" info " JAR : $JAR_PATH" info " 配置 : $CONFIG_DIR/" nohup java $JVM_OPTS \ -jar "$JAR_PATH" \ --spring.profiles.active="$ACTIVE_PROFILE" \ --spring.config.location="$CONFIG_DIR/" \ >> "$LOG_DIR/console-$(date '+%Y%m%d%H%M').log" 2>&1 & echo $! > "$PID_FILE" sleep 5 if is_running; then info "✅ $APP_NAME 启动成功 (PID: $(get_pid))" else error "❌ $APP_NAME 启动失败!请查看日志: $LOG_DIR/" exit 1 fi } # 主逻辑:运行就是重启/启动 do_start SCRIPT # 创建备份脚本 cat > "$SVC_DIR/bin/backup.sh" << 'SCRIPT' #!/bin/bash #=================================================== # 服务备份脚本 # 用法: ./backup.sh # 功能: 把当前 JAR 包复制到 backup 目录,带时间戳 #=================================================== APP_NAME="这里改成你的服务名" JAR_NAME="这里改成你的jar包名.jar" APP_DIR=$(cd "$(dirname "$0")/.." && pwd) JAR_PATH="$APP_DIR/app/$JAR_NAME" BACKUP_DIR="/opt/app/backup/$APP_NAME" RED='\033[0;31m'; GREEN='\033[0;32m'; NC='\033[0m' info() { echo -e "${GREEN}[INFO]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; } if [ ! -f "$JAR_PATH" ]; then error "找不到 JAR 文件: $JAR_PATH" exit 1 fi mkdir -p "$BACKUP_DIR" # 备份文件名加上时间戳,方便识别 BACKUP_NAME="${JAR_NAME}.bak_$(date '+%Y%m%d%H%M%S')" cp "$JAR_PATH" "$BACKUP_DIR/$BACKUP_NAME" info "✅ 备份完成: $BACKUP_DIR/$BACKUP_NAME" # 只保留最近 5 个备份,旧的自动清理 ls -t "$BACKUP_DIR"/*.bak_* 2>/dev/null | tail -n +6 | xargs -r rm -f info "已清理旧备份(保留最近 5 个)" SCRIPT # 设置执行权限 chmod +x "$SVC_DIR/bin/restart.sh" chmod +x "$SVC_DIR/bin/backup.sh" echo "" echo "✅ 目录结构初始化完成!" echo "" echo "📁 $SVC_DIR/" echo " ├── app/ ← 把 JAR 包放这里" echo " ├── config/ ← 把 application-prod.yml 放这里" echo " ├── logs/ ← 日志会自动写到这里" echo " ├── tmp/ ← PID 文件自动管理" echo " └── bin/" echo " ├── restart.sh ← 启动/重启脚本" echo " └── backup.sh ← 备份脚本" echo "" echo "⚡ 下一步:" echo " 1. 把 JAR 包上传到 $SVC_DIR/app/" echo " 2. 把配置文件放到 $SVC_DIR/config/" echo " 3. 修改 restart.sh 和 backup.sh 顶部的 APP_NAME 和 JAR_NAME" echo " 4. 运行 $SVC_DIR/bin/restart.sh 启动服务"
使用方式非常简单:
▼bash复制代码# 初始化一个新服务的目录结构 ./init-structure.sh user-service # 再初始化另一个服务 ./init-structure.sh api-gateway
核心脚本详解
初始化完成后,每个服务的 bin/ 目录下会有两个脚本。这两个脚本就是日常运维的全部武器,不需要记复杂的命令,会这两个就够了。
脚本一:restart.sh —— 启动/重启服务
这是最核心的脚本,日常 90% 的操作都用它。它的工作逻辑很简单:服务没启动就启动,已经在跑就先停再启。你不需要关心服务当前是什么状态,运行就完了。
▼mermaid复制代码graph TD A["运行 ./restart.sh"] --> B{"检查 PID 文件<br/>服务是否在运行?"} B -->|"没有 PID 文件<br/>或进程已死"| C["直接启动"] B -->|"有 PID 且进程活着"| D["kill -15 发送停止信号<br/>给应用 30 秒优雅关闭"] D --> E{"30 秒内停了吗?"} E -->|是| F["确认已停"] E -->|否| G["kill -9 强制杀掉<br/>防止无限卡住"] G --> F F --> H["等待 2 秒<br/>确保端口释放"] H --> C C --> I["nohup java -jar xxx.jar &<br/>后台启动服务"] I --> J["记录 PID 到 tmp/app.pid"] J --> K["等待 5 秒检查<br/>确认启动成功"] K --> L["✅ 服务已就绪"]
几个你可能好奇的设计细节,用大白话解释一下:
为什么要 PID 文件? PID 文件就是记录"当前运行的进程号"的小文件,放在 tmp/app.pid 里。脚本靠它判断服务是否在运行——有 PID 文件且对应进程还活着,说明服务在跑;没有 PID 文件或进程已经死了,说明服务停了。这比每次都 ps -ef | grep xxx 去查要快得多、准得多。
为什么先 kill -15 再 kill -9? kill -15(SIGTERM)相当于"礼貌地请应用退出",Spring Boot 收到这个信号后会执行清理工作——关闭数据库连接、保存缓存、写完日志——然后优雅退出。kill -9(SIGKILL)相当于"直接拔电源",进程立刻消失,什么清理都来不及做。所以我们的策略是:先礼貌请退,等 30 秒,如果还不走再强制拖走。
为什么用 nohup? nohup 的意思是"不要因为终端关闭就停掉程序"。如果没有 nohup,你 SSH 登录服务器启动 Java 进程,关掉 SSH 窗口后进程就跟着死了。加了 nohup,即使你关掉终端、断开 SSH,服务依然在后台运行。
为什么 sleep 5 秒再检查? Java 应用启动需要时间——加载类、初始化 Spring 容器、连接数据库……这些不是瞬间完成的。等 5 秒再检查进程是否还活着,可以过滤掉"刚启动就崩了"的情况。如果 5 秒后进程还活着,基本可以认为启动成功了。
脚本二:backup.sh —— 备份当前版本
更新版本之前先备份,这是运维的基本素养。万一新版本有 bug,把备份的旧 JAR 包换回来就能恢复,比重新构建快得多。
▼mermaid复制代码graph LR A["运行 ./backup.sh"] --> B["把当前 JAR 复制到<br/>/opt/app/backup/服务名/"] B --> C["文件名加上时间戳<br/>user-service.jar.bak_202606161030"] C --> D["检查备份数量"] D --> E{"超过 5 个?"} E -->|是| F["自动删除最老的<br/>只保留最近 5 个"] E -->|否| G["✅ 备份完成"] F --> G
备份文件名带时间戳,这样你能清楚地知道每个备份是什么时候做的。自动保留最近 5 个备份,太老的自动清理,防止备份目录无限膨胀。
日常运维——就这两条命令
| 场景 | 命令 | 效果 |
|---|---|---|
| 首次启动服务 | ./restart.sh | 检测到没在运行,直接启动 |
| 重启服务 | ./restart.sh | 检测到在运行,先停再启 |
| 切换到开发环境 | ./restart.sh dev | 以 dev 环境启动(默认是 prod) |
| 更新版本前备份 | ./backup.sh | 当前 JAR 包安全保存到 backup 目录 |
就这些,没有复杂的参数,没有多余的子命令。
发布更新完整流程
下面这张图展示了从"开发完成"到"上线运行"的完整操作流程,每一步都配有对应的命令:
▼mermaid复制代码graph TD A["1️⃣ 备份当前版本"] --> B["2️⃣ 停止并重启服务<br/>(restart.sh 会自动停旧的启新的)"] B --> C["3️⃣ 上传新 JAR 包<br/>覆盖 app/ 下的旧文件"] C --> D["4️⃣ 再次运行 restart.sh<br/>用新 JAR 启动服务"] D --> E{"5️⃣ 检查日志<br/>服务是否正常?"} E -->|"正常"| F["✅ 发布完成"] E -->|"异常"| G["6️⃣ 从 backup 恢复旧版本<br/>cp backup/xxx.jar app/"] G --> H["再次 restart.sh"] H --> F
对应的命令操作:
▼bash复制代码# 1. 先备份(防止翻车) cd /opt/app/user-service/bin ./backup.sh # 2. 上传新 JAR 包到 app 目录(从你本地电脑上传到服务器) scp target/user-service-1.0.1.jar user@server:/opt/app/user-service/app/user-service.jar # 3. 运行 restart.sh,自动停旧版本、启新版本 ./restart.sh # 4. 看一眼日志,确认启动正常 tail -f ../logs/console-*.log # 5. 万一新版本有问题,从备份恢复 cp /opt/app/backup/user-service/user-service.jar.bak_20260616103000 \ /opt/app/user-service/app/user-service.jar ./restart.sh
外部配置文件说明
你可能注意到,启动命令里有一行 --spring.config.location=../config/。这是什么意思?
Spring Boot 项目打包时,配置文件(application.yml)会被打进 JAR 包里。但生产环境的数据库地址、密码跟开发环境肯定不一样,你不可能为每台服务器单独打一个 JAR 包。--spring.config.location 就是告诉 Spring Boot:"别用 JAR 包里面的配置了,用我指定目录下的配置文件。" 这样 JAR 包只打一次,不同服务器放不同的 application-prod.yml 就行。
▼yaml复制代码# /opt/app/user-service/config/application-prod.yml server: port: 8081 spring: datasource: url: jdbc:mysql://10.0.0.100:3306/user_db?useSSL=true username: ${DB_USER} # 也可通过环境变量注入,避免密码明文写在文件里 password: ${DB_PASSWORD} hikari: maximum-pool-size: 20 logging: file: name: logs/application.log logback: rollingpolicy: max-file-size: 50MB max-history: 30
这种方式的好处用三句话总结:
- 改配置不需要重新打包 — 直接编辑 yml 文件,运行
./restart.sh就生效 - 不同服务器可以有不同配置 — JAR 包保持一致,配置各管各的
- 敏感信息不入代码仓库 — 数据库密码、API 密钥只存在于服务器上,不会泄露到 Git
2.3 方式二:systemd 服务管理(适合需要开机自启的场景)
如果你的服务需要开机自启或与系统服务统一管理,可以将脚本注册为 systemd 服务:
▼ini复制代码# /etc/systemd/system/user-service.service [Unit] Description=User Service (Spring Boot) After=network.target [Service] Type=forking User=deploy WorkingDirectory=/opt/app/user-service ExecStart=/opt/app/user-service/bin/app.sh start ExecStop=/opt/app/user-service/bin/app.sh stop ExecReload=/opt/app/user-service/bin/app.sh restart PIDFile=/opt/app/user-service/tmp/app.pid Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target
▼bash复制代码# 注册并启动 sudo systemctl daemon-reload sudo systemctl start user-service sudo systemctl enable user-service # 开机自启 # 查看状态和日志 sudo systemctl status user-service sudo journalctl -u user-service -f
提示:使用
Type=forking配合脚本中的后台启动(nohup &),systemd 能通过 PIDFile 正确追踪进程状态。
2.4 方式三:Docker 容器化部署
什么是 Docker?为什么要用 Docker?
先说一个很多人都会遇到的痛点:"在我电脑上明明能跑啊!"——你本地开发得好好的,一部署到服务器就各种报错,JDK 版本不对、系统依赖缺失、文件路径写死……这些问题说到底都是"环境不一致"造成的。
Docker 就是来解决这个问题的。它的核心思路很简单:把你的应用和它需要的所有依赖(JDK、系统库、配置文件)一起打包成一个"镜像",这个镜像就像一个密封的集装箱——不管你把它搬到哪台机器上,只要装了 Docker,打开就能跑,环境一模一样,不会再有"在我电脑上能跑"的尴尬。
用大白话来打几个比方:
- 镜像(Image) = 装好软件的系统盘。比如
mysql:8.0这个镜像,就是一个已经装好了 MySQL 8.0 的"系统盘",你拿过来直接用就行,不用自己安装配置。 - 容器(Container) = 用系统盘装好的、正在运行的电脑。一个镜像可以"装"出多个容器,就像一张系统盘可以装多台电脑。
- Dockerfile = 安装说明书。告诉 Docker 怎么一步步打包你的应用——先装 JDK,再复制代码,再编译打包……每一步都写清楚。
- Docker Compose = 批量启动器。你的项目通常不只是一个应用,还有数据库、缓存、Nginx……Docker Compose 让你用一个 YAML 文件定义所有服务,一条命令全部拉起来。
▼mermaid复制代码graph LR A["Dockerfile<br/>安装说明书"] -->|docker build| B["镜像 Image<br/>打包好的系统盘"] B -->|docker run| C["容器 Container<br/>正在运行的服务"] B -->|docker run| D["容器 Container<br/>另一个实例"] B -->|docker run| E["容器 Container<br/>又一个实例"] F["docker-compose.yml<br/>批量编排文件"] -->|docker-compose up| G["一键启动<br/>所有服务"]
Docker vs 传统部署的直观对比:传统部署就像自己买砖、买水泥、雇工人盖房子,每换一块地就得重新来;Docker 部署就像买了一个移动板房,里面水电齐全,搬到哪里都能直接住。
本节将分两部分讲解:第一部分是测试环境单节点部署(入门友好,快速上手),第二部分是生产级多节点集群部署(正式上线用,高可用保障)。
第一部分:测试环境单节点部署
什么是单节点部署?
单节点部署就是把所有服务——应用、数据库、缓存、Nginx——都装在同一台服务器上,各自跑在独立的 Docker 容器里。就像把厨房、卧室、客厅都放在一个房间,虽然挤了点,但一个人住完全够用。
适合场景:自己开发测试、功能验证、给客户做演示。
不适合场景:正式对外提供服务。因为一旦这台机器宕机(断电、硬件故障、网络中断),所有服务全挂,用户完全无法访问。生产环境必须用多节点,后面会讲。
部署架构
下面这张图展示了单节点部署时,各容器之间的关系和调用链路。用户的请求先到 Nginx(门面),Nginx 根据请求路径把请求转发给对应的应用容器,应用容器再去连接数据库和缓存。
▼mermaid复制代码graph TB subgraph "单台服务器 192.168.1.100" subgraph "Docker 环境" nginx["Nginx 容器<br/>反向代理 / 流量分发<br/>端口: 80 / 443"] app1["应用容器 1<br/>Spring Boot 服务<br/>端口: 8080"] app2["应用容器 2<br/>Spring Boot 服务<br/>端口: 8081"] redis["Redis 容器<br/>缓存服务<br/>端口: 6379"] mysql["MySQL 容器<br/>数据库<br/>端口: 3306"] end end user["用户请求"] --> nginx nginx -->|"转发 /api/"| app1 nginx -->|"转发 /api/"| app2 app1 --> redis app1 --> mysql app2 --> redis app2 --> mysql
数据流解读:用户访问网站 → 请求先到 Nginx(统一入口)→ Nginx 把 API 请求转发给 Spring Boot 容器 → Spring Boot 从 Redis 读缓存,缓存没有就去 MySQL 查 → 查到后返回给用户,同时写入 Redis 缓存。
前置准备
在开始部署之前,需要先在服务器上安装 Docker 和 Docker Compose。就像做饭之前要先把灶台和锅碗瓢盆准备好一样,这些是后续所有操作的基础。
| 步骤 | 命令 | 大白话解释 |
|---|---|---|
| 安装 Docker | curl -fsSL https://get.docker.com | bash | 从 Docker 官网拉一个安装脚本,自动帮你装好 Docker 引擎。就像让专业师傅上门装灶台,一条命令搞定 |
| 安装 Docker Compose | sudo yum install docker-compose -y | Docker Compose 是 Docker 的"批量管家",让你用一个配置文件同时管理多个容器。没有它你就得一个一个手动启动 |
| 验证安装 | docker --version | 看一眼版本号,确认安装成功了。如果报 command not found,说明没装上,回头检查 |
| 启动 Docker 服务 | sudo systemctl start docker | Docker 装好了但还没运行,这条命令相当于"开灶" |
| 设置开机自启 | sudo systemctl enable docker | 服务器重启后 Docker 自动启动,不用你每次手动开。生产环境必须设置,不然机器重启后服务就断了 |
Docker Compose 配置文件详解
这是整个部署的核心——docker-compose.yml。它就像一张"施工图纸",告诉 Docker 要启动哪些服务、每个服务怎么配置、服务之间怎么关联。下面逐段讲解,每一行都配上大白话解释。
▼yaml复制代码# version 表示 Compose 文件格式的版本号 # 不同版本支持的功能不同,3.8 是目前最常用的稳定版本 version: '3.8' # services 下面定义所有需要运行的容器 # 每一个 service 就是一个独立的容器,可以理解为"一台虚拟小电脑" services: # ---- MySQL 数据库服务 ---- # 数据库是整个应用的数据仓库,所有业务数据都存在这里 mysql: # image 指定使用哪个"系统盘"来创建容器 # mysql:8.0 就是一个已经装好 MySQL 8.0 的系统盘,拿来即用 image: mysql:8.0 # container_name 给容器起个名字,方便后续管理 # 不指定的话 Docker 会自动生成一个随机名字(如 mystifying_tesla),不好记 container_name: test-mysql # restart: always —— 容器挂了自动重启 # 比如数据库进程崩了,Docker 会自动把它拉起来,不用你半夜爬起来手动重启 restart: always # environment 设置容器内部的环境变量 # 这些变量在 MySQL 首次启动时生效,帮你自动完成初始化配置 environment: MYSQL_ROOT_PASSWORD: root123456 # root 超级用户的密码,权力最大 MYSQL_DATABASE: myapp # 首次启动自动创建这个数据库,省得你手动建 MYSQL_USER: appuser # 创建一个普通用户,日常操作用这个,不用 root MYSQL_PASSWORD: apppass123 # 普通用户的密码 # ports 端口映射:把容器内部的端口"暴露"到宿主机 # 格式是 "宿主机端口:容器端口" # "3306:3306" 意思是:访问服务器的 3306 端口,就等于访问容器内的 3306 端口 ports: - "3306:3306" # volumes 目录挂载:把宿主机的目录"绑定"到容器内部 # 这是数据持久化的关键——默认情况下,容器删除后内部数据就没了 # 挂载后,数据实际存在宿主机上,容器删了重建,数据还在 volumes: # 数据库文件存到宿主机 ./data/mysql 目录 # 容器内 /var/lib/mysql 是 MySQL 存数据的地方,映射出来就不怕丢了 - ./data/mysql:/var/lib/mysql # 把初始化 SQL 脚本挂载进去 # MySQL 首次启动时会自动执行这个目录下的 .sql 文件,帮你建表、灌初始数据 - ./init.sql:/docker-entrypoint-initdb.d/init.sql # ---- Redis 缓存服务 ---- # Redis 是内存数据库,用来缓存热点数据,减轻数据库压力 # 就像你把常用文件放在桌面,不用每次都去文件柜翻 redis: image: redis:7-alpine # alpine 版本基于 Alpine Linux,体积只有正常版的 1/5 container_name: test-redis restart: always ports: - "6379:6379" # command 覆盖容器默认的启动命令 # 默认 Redis 启动是不设密码的,这里加上密码防止被人白嫖 command: redis-server --requirepass redis123 volumes: - ./data/redis:/data # 把 Redis 的持久化文件也挂出来,防止丢失 # ---- 应用服务(Spring Boot 后端)---- # 这是你的核心业务应用,处理用户的请求、执行业务逻辑 app: # build 表示不从仓库拉镜像,而是根据 Dockerfile 自己构建 # 就像不买现成的家具,而是按图纸自己打 build: context: . # Dockerfile 在当前目录下 dockerfile: Dockerfile # 指定 Dockerfile 文件名 container_name: test-app restart: always ports: - "8080:8080" # depends_on 定义启动顺序 # 意思是:先启动 mysql 和 redis,再启动 app # 不然应用启动时连不上数据库就报错了 # 注意:depends_on 只保证启动顺序,不保证 mysql 已经"准备好接受连接" # 如果应用启动太快连不上数据库,可以在应用里加重试逻辑 depends_on: - mysql - redis # environment 注入环境变量 # 这些变量会覆盖 Spring Boot 配置文件中的对应项 # 好处:不用改代码和配置文件,只需改环境变量就能切换数据库连接等配置 environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/myapp?useSSL=false&serverTimezone=Asia/Shanghai # 注意这里的 host 写的是 "mysql" 而不是 IP # 因为 Docker Compose 会自动创建一个内部网络,服务名就是域名 # 应用容器访问 "mysql:3306" 就能连到 MySQL 容器,不用管 IP SPRING_DATASOURCE_USERNAME: appuser SPRING_DATASOURCE_PASSWORD: apppass123 SPRING_REDIS_HOST: redis # 同理,"redis" 就是 Redis 容器的域名 SPRING_REDIS_PASSWORD: redis123 # ---- Nginx 反向代理 ---- # Nginx 是整个系统的"前台门面",用户只跟它打交道 # 它负责:1. 接收用户请求 2. 转发给后端应用 3. 返回结果给用户 # 这样用户只需要访问 80 端口,不用记住后端各种端口号 nginx: image: nginx:alpine container_name: test-nginx restart: always ports: - "80:80" # HTTP 端口,用户访问的就是这个 - "443:443" # HTTPS 端口,配置 SSL 后使用 volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro # :ro = read-only 只读挂载 # Nginx 配置文件挂进来,修改后 nginx -s reload 即可生效 # :ro 防止容器内的进程意外修改配置文件 depends_on: - app # 等应用启动后再启动 Nginx
关键概念说明:
- 端口映射(ports):容器是一个封闭的小世界,外部默认访问不到。端口映射就是在容器上"开一扇窗",让外部流量能进来。
"8080:8080"就是把容器的 8080 端口暴露到宿主机的 8080 端口。- 目录挂载(volumes):容器内部的数据默认是临时的,容器一删就没了。挂载就是把宿主机的目录"绑定"到容器内,这样数据实际存在宿主机上,容器重建后数据还在。
- 服务名即域名:Docker Compose 自动创建内部网络,服务名就是域名。比如
mysql:3306就能访问 MySQL 容器,不用写 IP。
应用 Dockerfile 详解
Dockerfile 是打包应用的"菜谱",告诉 Docker 怎么把你的源代码变成一个可运行的镜像。这里用的是多阶段构建——先在一个"厨房"里编译代码,再把编译好的"成品菜"端到另一个干净的"盘子"里。这样做的好处是最终镜像很小,不会把编译工具链也带进去。
▼dockerfile复制代码# ============ 阶段1:构建 ============ # 这个阶段就像一个"专用厨房",里面有 Maven 和 JDK,专门用来编译代码 # 构建完成后这个"厨房"就被丢弃了,不会出现在最终镜像里 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app # 先复制 pom.xml 并下载依赖 # 这一步单独写,是为了利用 Docker 的缓存机制: # 只要 pom.xml 没变,下次构建就会跳过依赖下载,节省大量时间 COPY pom.xml . RUN mvn dependency:go-offline -B # 把所有依赖下载到本地缓存 # 再复制源代码并编译 COPY src ./src RUN mvn clean package -DskipTests -B # 打包成 JAR,跳过测试(测试在 CI 阶段已经跑过了) # ============ 阶段2:运行 ============ # 这是最终镜像,只包含运行应用所需的最小环境 # 用 JRE 而不是 JDK,因为运行时不需要编译器,体积小很多 FROM eclipse-temurin:17-jre-alpine WORKDIR /app # 安全最佳实践:在容器内创建一个普通用户来运行应用 # 默认容器以 root 用户运行,一旦被攻破,攻击者就拥有了 root 权限 # 用普通用户运行,即使被攻破,危害也有限 RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser # 从 builder 阶段把编译好的 JAR 包复制过来 # --from=builder 表示从名为 builder 的阶段复制 COPY --from=builder /app/target/*.jar app.jar # 健康检查:Docker 每 30 秒访问一次 /actuator/health 端点 # 连续 3 次失败就认为容器不健康,触发重启策略 # 就像定时给容器"量体温",发烧了就送医院 HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD wget -qO- http://localhost:8080/actuator/health || exit 1 # EXPOSE 声明容器对外提供服务的端口 # 这只是一个"文档说明",并不会实际发布端口,真正的端口映射在 docker-compose.yml 中 EXPOSE 8080 # ENTRYPOINT 容器启动时执行的命令 # 这里启动 Java 应用,配置了基本 JVM 参数 ENTRYPOINT ["java", \ "-Xms512m", "-Xmx1024m", \ # 初始/最大堆内存 "-Djava.security.egd=file:/dev/./urandom", \ # 加速 SecureRandom 初始化 "-jar", "app.jar"]
启动与运维
万事俱备,只欠东风。配置文件写好后,按照下面的流程图操作即可:
▼mermaid复制代码graph TD A["编写 docker-compose.yml"] --> B["docker-compose up -d<br/>后台启动所有服务"] B --> C["docker-compose ps<br/>查看各容器运行状态"] C --> D{"服务是否正常?"} D -->|正常| E["docker-compose logs -f app<br/>实时查看应用日志"] D -->|异常| F["docker-compose logs mysql<br/>排查数据库日志"] F --> G["修改配置后<br/>docker-compose restart"] G --> C E --> H["测试完成<br/>docker-compose down<br/>停止并删除所有容器"]
| 命令 | 大白话解释 |
|---|---|
docker-compose up -d | "一键开火"——根据配置文件启动所有容器。-d 是后台运行的意思,不加的话当前终端会被占满,关掉终端服务就停了 |
docker-compose ps | "点名"——看看哪些容器在跑、哪些挂了,以及各自的端口映射 |
docker-compose logs -f 服务名 | "监听"——实时查看某个服务的日志输出,-f 表示持续跟踪(跟 tail -f 一个意思),按 Ctrl+C 退出 |
docker-compose restart 服务名 | "重启"——某个服务改了配置文件后,重启让新配置生效 |
docker-compose down | "收工"——停掉并删除所有容器和网络。注意:数据卷(volumes 挂载的目录)不会删,数据库数据还在 |
docker-compose down -v | "连根拔起"——连数据卷一起删!数据库数据永久丢失,慎用! |
docker exec -it test-mysql bash | "进入容器内部"——像远程登录一样进入 MySQL 容器的命令行,可以直接执行 SQL |
第二部分:生产级多节点集群部署
为什么需要多节点?
单节点部署有一个致命问题:单点故障。那台机器一旦宕机——不管是断电、硬盘坏了、还是网络中断——你的整个系统就瘫痪了。用户访问不了,订单下不了,数据查不到,损失可能按分钟计算。
生产环境必须做到高可用:多台机器协同工作,任何一台挂了,其他机器自动接管,用户完全感知不到。这就好比你开了一家店,只有一个收银员,他请假了店就得关门;但如果你有三个收银员,谁请假都不影响营业。
多节点集群要解决的问题有三个:
- 应用层高可用:部署多个应用实例,一个挂了其他还在
- 数据库高可用:主库挂了,从库自动升级为主库,数据不丢
- 入口层高可用:负载均衡器也要做双机热备,不能它自己成了单点
多节点集群架构
下面这张图展示了生产环境的完整集群架构。从上往下看:用户请求先到负载均衡层,再分发到应用集群,应用集群再去访问缓存和数据库。每一层都做了高可用设计,不存在单点故障。
▼mermaid复制代码graph TB subgraph "负载均衡层 — 系统大门" lb1["Keepalived + Nginx<br/>主负载均衡器<br/>192.168.1.10"] lb2["Keepalived + Nginx<br/>备负载均衡器<br/>192.168.1.11"] vip["虚拟 IP VIP<br/>192.168.1.100<br/>用户只访问这个 IP"] end subgraph "应用服务集群 — 干活的" app1["Node-1<br/>应用容器<br/>192.168.1.20"] app2["Node-2<br/>应用容器<br/>192.168.1.21"] app3["Node-3<br/>应用容器<br/>192.168.1.22"] end subgraph "缓存集群 — 记性好的" r1["Redis 主节点<br/>读写<br/>192.168.1.30"] r2["Redis 从节点<br/>只读备份<br/>192.168.1.31"] r3["Redis 从节点<br/>只读备份<br/>192.168.1.32"] sentinel["Redis Sentinel<br/>哨兵:监控主节点健康<br/>主节点挂了自动提拔从节点"] end subgraph "数据库集群 — 存档案的" m1["MySQL 主库<br/>读写<br/>192.168.1.40"] m2["MySQL 从库<br/>只读<br/>192.168.1.41"] m3["MySQL 从库<br/>只读<br/>192.168.1.42"] end subgraph "管理与监控 — 后勤保障" swarm["Swarm Manager<br/>集群总指挥"] monitor["Prometheus + Grafana<br/>监控面板 + 告警"] end user["用户请求"] --> vip vip -.->|"VIP 漂移"| lb1 vip -.->|"主挂了才切"| lb2 lb1 <-->|"VRRP 心跳检测<br/>互相确认对方还活着"| lb2 lb1 --> app1 lb1 --> app2 lb1 --> app3 app1 --> r1 app2 --> r1 app3 --> r1 r1 -->|"数据同步"| r2 r1 -->|"数据同步"| r3 sentinel -.->|"监控 & 故障转移"| r1 sentinel -.->|"监控"| r2 sentinel -.->|"监控"| r3 app1 --> m1 app2 --> m1 app3 --> m1 m1 -->|"主从复制"| m2 m1 -->|"主从复制"| m3 swarm -->|"调度管理"| app1 swarm -->|"调度管理"| app2 swarm -->|"调度管理"| app3 monitor -.->|"采集指标"| app1 monitor -.->|"采集指标"| app2
集群组件职责说明(大白话版)
| 组件 | 官方说法 | 大白话理解 |
|---|---|---|
| Docker Swarm | 容器集群编排工具 | 工地的总调度:决定哪个工人(节点)干什么活,有人请假自动找人顶上 |
| Keepalived + VIP | 负载均衡高可用方案 | 公司的总机号码:对外只有一个号码(VIP),背后两台机器一主一备。主机正常时它接电话,主机挂了备机无缝接替,客户完全不知道换人了 |
| Nginx 集群 | 七层反向代理与负载均衡 | 前台接待员:把来访的客户请求均匀分配给后台的工作人员(应用实例),谁闲就分给谁 |
| Redis 主从 + Sentinel | 缓存高可用与自动故障转移 | 记性好的团队:主节点负责记东西(读写),从节点抄一份备份。Sentinel 是"监工",盯梢主节点,一发现它倒下了立马提拔一个从节点上位 |
| MySQL 主从复制 | 数据库高可用与读写分离 | 档案室:主库存原件(写),从库存复印件(只读)。写操作只找主库,读操作分摊给从库,既安全又快 |
| Prometheus + Grafana | 指标采集、可视化与告警 | 体检中心:Prometheus 定时给每台机器量体温、测血压(采集指标),Grafana 把数据画成图表,体温超标自动发短信告警 |
第一步:初始化 Docker Swarm 集群
Docker Swarm 是 Docker 自带的集群管理工具,装了 Docker 就自带,不用额外安装。它的工作方式很简单:选一台机器当"管理者"(Manager),其他机器当"打工人"(Worker),管理者负责分配任务,打工人负责执行。
下面的时序图展示了从零搭建集群的完整过程:
▼mermaid复制代码sequenceDiagram participant ops as 运维人员 participant mgr as Manager 节点<br/>192.168.1.10 participant w1 as Worker 节点1<br/>192.168.1.20 participant w2 as Worker 节点2<br/>192.168.1.21 ops->>mgr: docker swarm init --advertise-addr 192.168.1.10<br/>初始化集群,我当老大 mgr-->>ops: 返回加入命令和 Token<br/>(相当于入队通行证) ops->>w1: docker swarm join --token SWMTKN-xxx 192.168.1.10:2377<br/>拿着通行证加入集群 w1-->>mgr: 注册成功,我来干活了 ops->>w2: docker swarm join --token SWMTKN-xxx 192.168.1.10:2377<br/>你也加入 w2-->>mgr: 注册成功,我也来了 ops->>mgr: docker node ls<br/>看看队里都有谁 mgr-->>ops: 显示节点列表<br/>Manager: Ready<br/>Worker1: Ready<br/>Worker2: Ready
命令详解(每条都讲清楚执行时机和效果):
| 命令 | 在哪台机器执行 | 大白话解释 |
|---|---|---|
docker swarm init --advertise-addr 192.168.1.10 | Manager 节点 | "我来当老大"——初始化集群。--advertise-addr 告诉其他节点"来找我报到"。执行后会输出一个 docker swarm join 命令,里面带着 Token,复制下来给其他机器用 |
docker swarm join --token SWMTKN-xxx 192.168.1.10:2377 | Worker 节点 | "我来打工"——拿着通行证加入集群。2377 是 Swarm 管理通信的端口,Token 是安全凭证,防止随便什么机器都来加入 |
docker node ls | Manager 节点 | "点名"——查看所有节点的状态。能看到每个节点是 Manager 还是 Worker、是正常(Ready)还是掉线(Down) |
docker node promote 节点名 | Manager 节点 | "提拔"——把 Worker 提升为 Manager,增加管理节点的冗余度。推荐至少 3 个 Manager 节点 |
docker node update --availability drain 节点名 | Manager 节点 | "休假"——把节点标记为"排空"模式,上面的容器会自动迁移到其他节点。常用于给机器做维护升级 |
第二步:部署服务栈
集群搭好了,接下来要把应用部署上去。这里用 Docker Compose 文件配合 Swarm 的 deploy 配置来实现多节点编排。和单节点的 docker-compose.yml 相比,最大的区别是多了 deploy 块——它告诉 Swarm 怎么在多台机器上分配容器。
▼yaml复制代码version: '3.8' services: app: image: registry.example.com/myapp:latest # 从私有镜像仓库拉取镜像 # deploy 块是 Swarm 模式专用配置 # 在普通 docker-compose up 中不生效,必须用 docker stack deploy 部署 deploy: # replicas 副本数量:同时运行几个应用实例 # 3 个副本分布在 3 台机器上,任何一台挂了,另外两台还能继续服务 replicas: 3 # update_config 滚动更新策略:怎么安全地更新到新版本 update_config: parallelism: 1 # 每次只更新 1 个副本,不会一口气全换 delay: 10s # 更新一个后等 10 秒,确认没问题再更新下一个 failure_action: rollback # 如果更新失败(新版本起不来),自动回滚到旧版本 # restart_policy 重启策略:容器挂了怎么处理 restart_policy: condition: on-failure # 只有异常退出才重启(正常退出不重启) max_attempts: 3 # 最多重试 3 次,避免无限重启(可能代码本身有问题) # placement 部署约束:容器应该放在哪种节点上 placement: constraints: - node.role == worker # 应用容器只部署在 Worker 节点上 # Manager 节点专注于管理工作,不跑业务,避免影响集群稳定性 environment: SPRING_PROFILES_ACTIVE: prod networks: - app-network nginx: image: nginx:alpine deploy: replicas: 2 # 2 个 Nginx 实例,互为备份 placement: constraints: - node.role == manager # Nginx 部署在 Manager 节点,作为统一入口 ports: - "80:80" - "443:443" configs: - source: nginx_config target: /etc/nginx/nginx.conf # configs 是 Swarm 专用的配置管理方式 # 比 volumes 更适合存储配置文件:可以版本化、可以滚动更新 configs: nginx_config: file: ./nginx-prod.conf # overlay 网络:允许不同机器上的容器互相通信 # 就像给分布在各楼层的员工配了对讲机,不用走公网 networks: app-network: driver: overlay
第三步:服务部署与运维命令
| 命令 | 大白话解释 |
|---|---|
docker stack deploy -c docker-compose.yml myapp | "全员上阵"——把配置文件中定义的所有服务部署到集群上。Swarm 会自动把容器分配到各个节点。myapp 是这个技术栈的名字,后续操作都靠它 |
docker stack ls | "看看部署了什么"——列出集群中所有的技术栈 |
docker stack services myapp | "看看各服务状态"——显示每个服务需要几个副本、当前跑了几个。如果 REPLICAS 显示 3/3 说明全在跑,2/3 说明有一个挂了 |
docker stack ps myapp | "细看每个副本"——显示每个副本具体跑在哪台机器上、运行了多久、是否正常 |
docker service logs -f myapp_app | "看聚合日志"——把分布在多台机器上的应用日志汇总显示,不用一台一台登录看 |
docker service scale myapp_app=5 | "紧急加人"——把应用副本从 3 个扩到 5 个,应对流量突增。Swarm 自动在可用节点上启动新容器 |
docker service update --image registry.example.com/myapp:v2.0 myapp_app | "升级装备"——把应用镜像更新到 v2.0 版本。Swarm 会按 update_config 中的策略逐个替换,保证服务不中断 |
docker stack rm myapp | "收队"——移除整个技术栈,所有容器停止并删除 |
第四步:多节点日常运维
集群部署不是一锤子买卖,日常巡检和及时处理问题同样重要。下面这张图展示了一个典型的日常运维流程:
▼mermaid复制代码graph TD A["每日巡检<br/>docker node ls<br/>检查所有节点状态"] --> B{"所有节点正常?"} B -->|是| C["查看服务状态<br/>docker stack services myapp"] B -->|否| D["排查故障节点<br/>docker node inspect 节点名"] D --> E["必要时驱逐节点<br/>docker node update --availability drain<br/>让容器迁走,安心修机器"] E --> F["修复后重新加入<br/>docker node update --availability active"] F --> A C --> G["查看 Grafana 监控面板"] G --> H{"资源使用率超阈值?"} H -->|是| I["扩容<br/>docker service scale myapp_app=N<br/>增加副本来分担压力"] H -->|否| J["记录巡检日志<br/>一切正常,收工"]
关键概念深入讲解:
节点故障自动恢复:当某个 Worker 节点宕机,Swarm 不需要你做任何事——它会自动发现该节点失联,然后在该节点上运行的容器迁移到其他健康节点。整个过程通常在几十秒内完成。这得益于 Swarm 的"期望状态协调"机制:你声明"我要 3 个应用副本",Swarm 会持续监控,发现只有 2 个在跑,就自动补上 1 个。就像你跟管家说"家里要常备 3 瓶牛奶",管家发现只剩 2 瓶了就自动去超市补货。
滚动更新的原理:更新时绝对不能把旧版本全停了再启动新版本(那叫"停机更新",用户会看到 502 错误)。滚动更新是逐个替换:先启动 1 个新版本容器 → 等健康检查通过 → 停掉 1 个旧版本容器 → 再启动第 2 个新版本容器……如此循环,直到全部替换完毕。用户在这个过程中完全感知不到服务中断。
日志聚合:在多节点集群中,同一个服务的 3 个副本可能分布在 3 台不同的机器上。docker service logs 会自动把所有副本的日志汇总到一起显示,不用你逐台 SSH 登录去看。
监控告警:推荐用 Prometheus 定时采集各节点的 CPU、内存、磁盘、网络指标,Grafana 以漂亮的图表展示。当某个指标超过阈值(比如内存使用率超过 90%),Alertmanager 会自动通过钉钉、邮件或短信发送告警。运维人员不需要一直盯着屏幕,有问题系统会主动通知你。
测试环境 vs 生产环境对比总结
| 维度 | 测试环境单节点 | 生产环境多节点集群 |
|---|---|---|
| 服务器数量 | 1 台 | 至少 3 台(推荐 5 台以上) |
| 容器编排 | Docker Compose | Docker Swarm |
| 高可用 | 无,单点故障 | 多副本自动故障转移 |
| 数据持久化 | 本地目录挂载 | NFS/分布式存储 |
| 负载均衡 | 无或单 Nginx | Nginx + Keepalived 双活 |
| 数据库 | 单实例 | 主从复制 + 读写分离 |
| 缓存 | 单 Redis | Redis 主从 + Sentinel |
| 监控告警 | 手动查看日志 | Prometheus + Grafana 自动告警 |
| 日志管理 | docker logs | ELK/Loki 集中日志平台 |
| 适用场景 | 开发调试、功能验证 | 正式对外提供服务 |
选型建议:如果你的项目是内部系统、用户量不大,单节点就够用了,别过度设计。等流量上来了、业务重要了,再升级到多节点也不迟。架构是演出来的,不是一步到位的。
2.5 JVM 调优参考
| 参数 | 说明 | 推荐值 |
|---|---|---|
-Xms | 初始堆内存 | 物理内存的 1/4 |
-Xmx | 最大堆内存 | 物理内存的 1/2,不超过 4G |
-XX:+UseG1GC | 使用 G1 垃圾回收器 | JDK 9+ 默认,低延迟场景推荐 |
-XX:MaxRAMPercentage=75.0 | 容器内按比例分配堆内存 | Docker 环境推荐 |
-XX:+HeapDumpOnOutOfMemoryError | OOM 时自动 dump 堆 | 生产必开,排查内存问题 |
-XX:HeapDumpPath | dump 文件路径 | 指向 logs 目录 |
▼bash复制代码# 脚本部署中的推荐 JVM 参数(已在 app.sh 中配置) JVM_OPTS="-Xms512m -Xmx1024m" JVM_OPTS="$JVM_OPTS -XX:+UseG1GC" JVM_OPTS="$JVM_OPTS -XX:+HeapDumpOnOutOfMemoryError" JVM_OPTS="$JVM_OPTS -XX:HeapDumpPath=$LOG_DIR/heapdump.hprof"
▼bash复制代码# Docker 环境下的推荐启动参数 java -XX:MaxRAMPercentage=75.0 \ -XX:+UseG1GC \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/app/logs/heapdump.hprof \ -jar app.jar
三、Python(FastAPI / Flask)项目部署

先说大白话:Python 项目部署和 Java 有什么不同?
很多学 Java 出身的同学第一次部署 Python 项目会懵:"Python 没有 main 方法、没有内置 Web 服务器,我到底在跑什么?"
先把这个困惑讲清楚 —— Java Spring Boot 自带 Tomcat,打个 JAR 包直接 java -jar 就能跑;但 Python 不是这样,它是解释型语言,没有"自带服务器"这个概念。要跑一个 HTTP API 服务,你需要自己搭一个 Web 服务器来"托管"你的 Python 代码。
这就引出了一整套工具和概念,我们用一张图来讲清楚:
▼mermaid复制代码graph LR A["用户请求"] --> B["Nginx<br/>反向代理"] B --> C["Gunicorn / Uvicorn<br/>WSGI / ASGI 服务器"] C --> D["FastAPI / Flask<br/>你的 Python 应用代码"] D --> E["数据库 / Redis"]
各层大白话解释:
| 层级 | 是什么 | 大白话理解 | 为什么需要 |
|---|---|---|---|
| Nginx | HTTP 反向代理服务器 | "前台接待员"——负责接客、分发请求、返回结果 | 直接把 Python 服务暴露到公网不安全,Nginx 做了一层缓冲和统一入口 |
| Gunicorn | WSGI HTTP 服务器 | "包工头"——负责启动和管理多个 Python 工作进程(Worker) | Python 代码自己跑不稳,需要个"包工头"来管理进程,挂了自动重启 |
| Uvicorn | ASGI 服务器(FastAPI 专用) | "技术工"——真正执行你的 async/await 代码的服务器 | FastAPI 基于 async/await,必须用支持 ASGI 的服务器才能发挥性能 |
| FastAPI/Flask | 你的业务代码 | "干活的程序员"——写接口、写业务逻辑的地方 | 这是你真正要部署的东西 |
一句话总结:部署 Python 项目 = 让你的代码跑在一个"包工头(Gunicorn)"管理下的"技术工(Uvicorn/Flask)"进程里,前面再挡一个"前台(Nginx)"。
3.1 项目结构示例
一个标准的 Python API 项目长这样,每个目录和文件都有明确分工:
▼text复制代码my-python-api/ ├── app/ │ ├── __init__.py # 包初始化文件(Python 包必须有这个) │ ├── main.py # 应用入口:FastAPI() 或 Flask() 实例创建的地方 │ ├── routers/ # 路由模块(接口定义放这里) │ │ └── user.py │ ├── models/ # 数据模型(Pydantic / SQLAlchemy 模型) │ │ └── user.py │ └── dependencies.py # 依赖注入(认证、数据库会话等) ├── requirements.txt # 依赖清单(pip install -r 安装的包列表) ├── gunicorn.conf.py # Gunicorn 配置文件(Worker 数、超时等) ├── Dockerfile # Docker 构建文件 └── .env # 环境变量(数据库密码、API Key 等,不要提交到 Git!)
为什么要这样组织?
app/是一个 Python 包,所有业务代码都在里面requirements.txt是 Python 项目的"购物清单"——列出所有需要安装的第三方库(类似 Java 的pom.xml)gunicorn.conf.py控制 Gunicorn 怎么跑你的应用:几个进程、超时多久、日志在哪.env存敏感信息,通过python-dotenv库加载,切记加入.gitignore,不要提交到代码仓库
3.2 方式一:虚拟环境直接部署
"虚拟环境"是 Python 的生态特色——它解决了"全局 Python 被不同项目互相污染"的问题。比如项目 A 需要 requests==2.28,项目 B 需要 requests==2.31,装在一起会冲突。虚拟环境就是给每个项目建一个独立的 Python 小房间,互不干扰。
部署步骤详解
跟着下面这张流程图操作,每一步都有大白话解释:
▼mermaid复制代码graph TD A["第一步:上传代码<br/>scp / git clone"] --> B["第二步:创建虚拟环境<br/>python3 -m venv venv"] B --> C["第三步:激活虚拟环境<br/>source venv/bin/activate"] C --> D["第四步:安装依赖<br/>pip install -r requirements.txt"] D --> E["第五步:测试启动<br/>看有没有报错"] E --> F{"启动成功?"} F -->|是| G["第六步:用 Gunicorn 启动<br/>正式跑起来"] F -->|否| H["排查错误<br/>看报错信息"] H --> D G --> I["第七步:配置 systemd<br/>开机自启 + 崩溃重启"]
第一步:上传代码到服务器
▼bash复制代码# 方式 A:从 Git 仓库拉取(推荐,版本可控) git clone https://github.com/yourname/my-python-api.git /opt/myapi cd /opt/myapi git checkout prod # 切换到生产分支 # 方式 B:用 scp 从本地上传(适合没用 Git 的项目) # 在本地机器执行: scp -r ./my-python-api user@your-server:/opt/myapi
第二步:创建虚拟环境
▼bash复制代码cd /opt/myapi # venv 是 Python 内置的虚拟环境工具,会在当前目录创建一个 venv/ 文件夹 # 里面是一个独立的 Python 解释器 + pip,跟系统 Python 完全隔离 python3 -m venv venv # 验证:激活后命令行提示符前面会出现 (venv) 字样 source venv/bin/activate which python # 应该显示 /opt/myapi/venv/bin/python(不是 /usr/bin/python)
大白话:
venv就像给这个项目单独配了一台"虚拟机",里面的 Python 是独立的,跟系统 Python 各装各的包,互不干扰。
第三步:安装依赖
▼bash复制代码# 激活虚拟环境后,pip 会自动把包装到 venv/ 里(不是装到系统) source venv/bin/activate pip install -r requirements.txt # 安装完成后,确认关键包都在 pip list | grep -E "fastapi|flask|gunicorn|uvicorn"
注意:FastAPI 生产环境需要额外装
gunicorn和uvicorn[standard]▼bash复制代码pip install gunicorn "uvicorn[standard]"
第四步:先测试启动,确认能跑起来
▼bash复制代码# ---------- FastAPI 项目 ---------- # uvicorn 是 FastAPI 的开发服务器,先用它测一下能不能跑 # --reload 表示代码改了自动重启(仅开发用,生产不要用!) uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload # 如果看到 "Uvicorn running on http://0.0.0.0:8000" 说明成功了 # 按 Ctrl+C 停掉,准备用 Gunicorn 正式跑 # ---------- Flask 项目 ---------- # Flask 内置的开发服务器(app.run)不能用于生产!只用来测试 flask --app app.main run --host 0.0.0.0 --port 8000
为什么开发服务器不能用于生产? 因为
uvicorn --reload或flask run是单线程、单进程的,只能同时处理一个请求,并发能力几乎为零,而且没有进程守护,挂了就是真挂了。
第五步:用 Gunicorn 正式启动(生产方式)
现在要用 Gunicorn("包工头")来管理你的应用进程了:
▼bash复制代码# ========== FastAPI 项目 ========== gunicorn app.main:app \ -w 4 \ # 启动 4 个 Worker 进程(具体几个看下面说明) -k uvicorn.workers.UvicornWorker \ # Worker 类型:FastAPI 必须用 UvicornWorker --bind 0.0.0.0:8000 \ # 监听所有网卡的 8000 端口 --timeout 120 \ # Worker 处理请求超过 120 秒就被强制杀掉重启 --access-logfile - \ # 访问日志输出到 stdout(Docker 场景推荐) --error-logfile - # 错误日志输出到 stdout # ========== Flask 项目 ========== gunicorn app.main:app \ -w 4 \ # 4 个 Worker 进程 -k gthread \ # Worker 类型:Flask 推荐用线程模式 --bind 0.0.0.0:8000 \ --timeout 120
-w 4 是什么意思?Worker 数量怎么定?
这是最容易困惑的地方,用大白话讲:gunicorn 启动后是这样的:
▼text复制代码Gunicorn(主进程,包工头) ├── Worker 1(干活的程序员) ├── Worker 2(干活的程序员) ├── Worker 3(干活的程序员) └── Worker 4(干活的程序员)
每个 Worker 是一个独立的 Python 进程,能同时处理请求。Worker 越多 = 并发能力越强,但也不是越多越好(内存有限)。
推荐公式:Worker 数 = (2 × CPU核心数) + 1
比如你的服务器是 2 核 CPU,就设 2 × 2 + 1 = 5 个 Worker。4 核就设 9 个。
Gunicorn 配置文件(推荐用文件代替命令行)
每次启动敲一大串参数太麻烦,也容易出错。推荐把配置写入 gunicorn.conf.py 文件:
▼python复制代码# gunicorn.conf.py # Gunicorn 会自动读取当前目录下的这个文件,不需要在命令行指定 -c import multiprocessing import os # ==================== 核心配置 ==================== # Worker 数量:公式 (2 × CPU核心数) + 1 # multiprocessing.cpu_count() 自动获取 CPU 核心数,不用硬编码 workers = multiprocessing.cpu_count() * 2 + 1 # 每个 Worker 的线程数(Flask 的 gthread 模式会用到,FastAPI 一般设 1) # FastAPI 是 async 的,一个进程就能处理大量并发,不需要多线程 # Flask 是同步的,一个请求占用一个线程,所以线程数可以设大一点(如 4) threads = 1 # FastAPI 用 1;Flask 用 4 # Worker 类型(非常重要!决定了你的应用怎么运行) # FastAPI → 必须用 "uvicorn.workers.UvicornWorker"(支持 async/await) # Flask → 推荐用 "gthread"(多线程模式,能更好利用 CPU) worker_class = "uvicorn.workers.UvicornWorker" # FastAPI 用这行 # worker_class = "gthread" # Flask 用这行(取消注释,注释掉上一行) # ==================== 网络配置 ==================== # 绑定地址:0.0.0.0 表示监听所有网卡(外网能访问) # 127.0.0.1 表示只监听本机(外网访问不了,一般不用) bind = "0.0.0.0:8000" # ==================== 超时配置 ==================== # Worker 处理请求的超时时间(秒) # 超过这个时间 Worker 还没处理完,Gunicorn 会强制 KILL 掉这个 Worker 并重启 # 设置太短:正常请求被误杀;设置太长:慢请求卡住 Worker 不释放 # 一般 API 接口 30-60 秒,有文件处理/导出的可以设 120-300 秒 timeout = 120 # 优雅重启超时(秒) # 当你 reload Gunicorn 时,旧 Worker 有这么多秒时间处理完手上的请求再退出 # 设为 0 表示立即强制退出(可能丢请求);设为 30 表示给 30 秒优雅收尾 graceful_timeout = 30 # ==================== 内存保护 ==================== # 每个 Worker 处理满 N 个请求后,自动重启自己 # 目的:防止代码里内存泄漏累积(Python 的 GC 不是万能的) # 比如设为 5000:每处理 5000 个请求,这个 Worker 就"自杀"重启,释放内存 max_requests = 5000 max_requests_jitter = 500 # 加一点随机抖动,避免所有 Worker 同时重启 # ==================== 日志配置 ==================== # 访问日志:记录每个请求的 URL、状态码、耗时 # "-" 表示输出到 stdout(Docker / systemd 场景推荐,日志由 Docker/systemd 统一管理) # 也可以写成文件路径:"/var/log/myapi/access.log" accesslog = "-" # 错误日志:记录异常、报错信息 errorlog = "-" # 日志级别:debug / info / warning / error / critical # 生产环境推荐 info 或 warning;debug 会打印太多日志影响性能 loglevel = "info" # ==================== 应用配置 ==================== # 预加载应用(True = 在 Worker fork 之前就把应用代码加载好) # 好处:多个 Worker 共享同一份代码内存,节省内存 # 坏处:代码里的全局变量在 fork 后不共享(每个 Worker 有自己的副本) # 一般设为 True;如果你的应用有全局状态且依赖共享内存,设为 False preload_app = True # 进程名称(方便用 ps 命令识别) proc_name = "myapi-gunicorn"
配置 systemd 服务(开机自启 + 崩溃重启)
直接 gunicorn ... 启动的进程,服务器重启后就停了,而且进程挂了不会自动重启。用 systemd 来管理就能解决这两个问题:
▼ini复制代码# /etc/systemd/system/myapi.service # systemd 是 Linux 的系统服务管理器,用来管理开机自启和进程守护 [Unit] Description=My Python API Service # After=network.target 表示:等网络服务就绪后再启动本服务(不然端口绑定会失败) After=network.target [Service] Type=notify # User / Group:用普通用户运行,不要用 root(安全!) User=www-data Group=www-data # WorkingDirectory:进程的工作目录(gunicorn.conf.py 要放在这个目录下) WorkingDirectory=/opt/myapi # Environment:设置环境变量,重点是让 systemd 知道去哪找 venv 里的 python/gunicorn Environment="PATH=/opt/myapi/venv/bin" # 如果有 .env 文件,可以通过 EnvironmentFile 加载: # EnvironmentFile=/opt/myapi/.env # ExecStart:启动命令(不需要 -c gunicorn.conf.py,因为 Gunicorn 会自动找同目录下的) ExecStart=/opt/myapi/venv/bin/gunicorn app.main:app # Restart=always:进程挂了就自动重启(always = 任何原因退出都重启) # RestartSec=5:重启前等 5 秒(避免频繁重启刷日志) Restart=always RestartSec=5 # 限制资源(防止内存泄漏拖垮整台服务器) # MemoryMax=1G:这个服务最多用 1GB 内存,超了会被系统杀掉 MemoryMax=1G [Install] # WantedBy=multi-user.target:多用户模式下自动启动(即正常的服务器运行级别) WantedBy=multi-user.target
启用并启动服务:
▼bash复制代码# 重新加载 systemd 配置(每次改了 .service 文件都要执行) sudo systemctl daemon-reload # 启动服务 sudo systemctl start myapi # 设置开机自启 sudo systemctl enable myapi # 查看状态(看是否 Active (running)) sudo systemctl status myapi # 查看日志(代替 tail -f /var/log/xxx.log) sudo journalctl -u myapi -f
3.3 方式二:Docker 容器化部署(推荐)
如果你的团队已经用 Docker 管理 Java/前端项目,Python 项目也用 Docker 部署是最省心的——环境完全一致,不会再有"我本地能跑"的问题。
FastAPI 项目的 Dockerfile(逐行大白话注释)
▼dockerfile复制代码# ==================== 阶段 1:安装依赖("厨房"阶段)==================== # 用 python:3.12-slim 作为构建环境 # slim 版本比 full 版本体积小很多(约 50MB vs 800MB),生产推荐 FROM python:3.12-slim AS builder # WORKDIR:设置工作目录为 /app(类似 cd /app,后面的命令都在这个目录下执行) WORKDIR /app # 先只复制 requirements.txt,再执行 pip install # 目的:利用 Docker 的缓存机制 # requirements.txt 不常变,这样依赖装好后,下次构建会直接用缓存,不用重新下载 COPY requirements.txt . # --no-cache-dir:不缓存下载的安装包(减小镜像体积) # --prefix=/install:把包装到 /install 目录下(方便下一阶段复制) RUN pip install --no-cache-dir --prefix=/install -r requirements.txt # ==================== 阶段 2:运行("上菜"阶段)==================== # 这个阶段是最终镜像,只保留运行所需的最小文件 FROM python:3.12-slim WORKDIR /app # 安全:创建非 root 用户来运行应用 # 默认用 root 跑容器,一旦被攻破,攻击者就有 root 权限,很危险 RUN groupadd -r appgroup && useradd -r -g appgroup appuser # 从 builder 阶段把安装好的依赖复制过来 # /install 目录下的所有文件会被复制到新镜像的 /usr/local 下 COPY --from=builder /install /usr/local # 复制应用代码到容器 COPY . . # 切换到普通用户(安全最佳实践) USER appuser # 健康检查:Docker 每隔一段时间访问一次 /health 接口 # 连续失败 3 次,Docker 就认为这个容器"不健康",触发重启策略 HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" # 声明容器对外暴露 8000 端口(文档作用,实际映射在 docker run -p 时指定) EXPOSE 8000 # 容器启动命令:用 Gunicorn 跑应用 # -w 4:4 个 Worker(容器内 CPU 核心数就是容器被分配的核数,可以用 os.cpu_count() 获取) # --bind 0.0.0.0:监听所有网卡(容器内必须写 0.0.0.0,写 127.0.0.1 外网访问不了) CMD ["gunicorn", "app.main:app", \ "-k", "uvicorn.workers.UvicornWorker", \ "-w", "4", \ "--bind", "0.0.0.0:8000", \ "--timeout", "120"]
Flask 项目的 Dockerfile(只改 CMD)
Flask 项目唯一区别是 Worker 类型不同,Gunicorn 用 gthread 模式:
▼dockerfile复制代码# ... 前面的 FROM / WORKDIR / COPY 都和 FastAPI 完全一样 ... CMD ["gunicorn", "app.main:app", \ "-w", "4", \ "--bind", "0.0.0.0:8000", \ "--timeout", "120"] # 注意:Flask 不需要 -k 参数,默认就是 sync worker # 如果想用多线程模式,加:-k gthread -t 4
构建与运行
▼bash复制代码# 构建镜像(在项目根目录执行,确保 Dockerfile 在当前目录) docker build -t myapi:latest . # 运行容器 docker run -d \ --name myapi \ -p 8000:8000 \ # 注入环境变量(覆盖代码中的配置,比改代码灵活) -e DATABASE_URL=postgresql://user:pass@db:5432/mydb \ -e REDIS_URL=redis://redis:6379/0 \ -e TZ=Asia/Shanghai \ # 挂载 .env 文件(适合不方便用 -e 注入的大量环境变量) -v /opt/myapi/.env:/app/.env:ro \ # 自动重启策略:除非手动停止,否则一直重启 --restart unless-stopped \ myapi:latest # 查看日志 docker logs -f myapi # 进入容器内部调试 docker exec -it myapi bash
四、前端(Vue / React)项目部署

4.1 核心认知
前端部署的本质:把源码构建成静态文件(HTML/CSS/JS),然后交给 Web 服务器(Nginx)对外提供服务。前端项目本身不需要运行时环境,Nginx 托管静态文件即可。
4.2 项目构建
▼bash复制代码# ---- Vue 项目(Vite 构建)---- npm install npm run build # 产物在 dist/ 目录 # ---- React 项目(Vite / CRA)---- npm install npm run build # 产物在 build/ 或 dist/ 目录
构建完成后,核心产物是 index.html + JS/CSS 静态资源文件。
4.3 方式一:Nginx 直接托管(最常用)
前端打包后到底生成了什么?
很多小白搞不清楚"构建"到底做了什么。用大白话来说:
构建 = 把你能看懂的 Vue/React 源码,翻译成浏览器能直接运行的 HTML + CSS + JS 文件
构建完成后,dist/ 文件夹里就是这些"翻译好"的文件:
▼text复制代码dist/ ├── index.html ← 入口页面,浏览器第一个加载的文件 ├── assets/ │ ├── index-abc123.js ← 你的业务代码(登录、下单等功能),带 hash 防缓存 │ ├── vendor-def.js ← 第三方库(Vue、React 等) │ └── style-xyz789.css ← 样式文件 └── favicon.ico
核心认知:Nginx 的工作就是"把这些文件发给浏览器",它不需要懂 Vue 或 React,它只负责"递文件"。
第一步:把 dist 上传到服务器,放在哪里?
推荐放在 /var/www/项目名/ 或 /opt/项目名/frontend/,两个方案对比:
| 方案 | 路径示例 | 适用场景 | 优点 |
|---|---|---|---|
/var/www/ | /var/www/myapp/ | 纯前端项目,Nginx 直接托管 | 符合 Linux 规范,权限清晰 |
/opt/项目名/ | /opt/myapp/frontend/dist/ | 前后端在同一台机器 | 前后端文件在一起,方便管理 |
推荐用 /var/www/ 方案,命令如下:
▼bash复制代码# 在本地构建(你的电脑上) npm run build # 构建完成后,dist/ 目录就生成了 # 把 dist/ 里的所有文件上传到服务器的 /var/www/myapp/ # scp 是 SSH 文件传输命令,把本地文件复制到远程服务器 scp -r dist/* user@your-server-ip:/var/www/myapp/ # ---- 或者,在服务器上直接构建(推荐) ---- ssh user@your-server-ip cd /var/www/myapp git pull origin main # 拉最新代码 npm install # 安装依赖 npm run build # 构建,产物在 dist/
小贴士:如果
npm run build时提示npm: command not found,说明服务器上还没装 Node.js,参考"一、部署前置"的 1.2 节安装。
第二步:Nginx 配置托管——逐行大白话解释
Nginx 要做的只有一件事:当用户访问你的网站时,把 dist/ 里的文件发给浏览器。
下面是完整配置,每一行都配有"大白话解释":
▼nginx复制代码# /etc/nginx/conf.d/myapp.conf # 这是 Nginx 的"虚拟主机"配置文件 # 一个文件对应一个网站,可以有多个网站同时跑在 80 端口 server { # listen 80:监听 80 端口(HTTP 默认端口) # 用户访问 http://你的域名 或 http://服务器IP,Nginx 就会收到请求 listen 80; # server_name:这个配置响应哪个域名的请求 # 比如 server_name www.example.com,只有访问 www.example.com 才会走这个配置 # 如果写 _(下划线),表示"匹配所有域名",适合临时测试 server_name www.example.com; # root:指定"网站根目录" # Nginx 收到请求后,会去这个目录下找文件 # 比如用户访问 /logo.png,Nginx 就去 /var/www/myapp/logo.png 找 root /var/www/myapp; # index:指定"默认首页" # 用户访问 / 时,Nginx 自动返回 index.html index index.html; # ---- 最关键配置:SPA 路由支持 ---- # 问题:Vue/React 是单页应用,路由是前端控制的 # 比如用户直接访问 /user/123,Nginx 会去找 /var/www/myapp/user/123 这个文件 # 但这个文件不存在!因为是前端路由,实际只有一个 index.html # # try_files 的作用: # 1. 先找 $uri(比如 /user/123 这个文件)——找不到 # 2. 再找 $uri/(比如 /user/123/ 目录)——找不到 # 3. 最后返回 /index.html(交给前端路由处理) # # 这一步是前端部署最容易出错的地方!配错了就会出现"刷新页面 404" location / { try_files $uri $uri/ /index.html; } # ---- 静态资源缓存策略 ---- # 带 hash 的 JS/CSS 文件(比如 index-abc123.js)内容不会变 # 浏览器可以缓存 1 年,下次直接读本地,不用再下载 location /assets/ { # expires 1y:告诉浏览器"这个文件 1 年内不用再来问我有没有更新" expires 1y; # Cache-Control "public, immutable":公开缓存,且内容不可变(因为有 hash) add_header Cache-Control "public, immutable"; } # ---- index.html 不缓存 ---- # index.html 是入口文件,每次发版都可能变 # 必须让浏览器每次都来问服务器"有没有新版本" location = /index.html { # expires -1:过期时间是"过去"(立即过期),浏览器每次都重新请求 expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate"; } # ---- Gzip 压缩 ---- # 把 JS/CSS 文件压缩后再发给浏览器,传输速度快 3-5 倍 gzip on; # gzip_types:指定哪些类型的文件需要压缩 # text/plain text/css application/javascript 是前端最常用的 gzip_types text/plain text/css application/json application/javascript text/xml; # gzip_min_length:小于 1024 字节的文件不压缩(压缩收益太低) gzip_min_length 1024; # ---- 安全头(可选但推荐)---- # 防止被嵌入 iframe 钓鱼 add_header X-Frame-Options "SAMEORIGIN" always; # 防止浏览器猜测 MIME 类型(安全加固) add_header X-Content-Type-Options "nosniff" always; }
配置文件写完后,让 Nginx 重新加载配置:
▼bash复制代码# 先检查配置文件语法是否正确(很重要!写错了 Nginx 会启动失败) sudo nginx -t # 语法 OK 后,平滑重载配置(不会中断正在处理的请求) sudo nginx -s reload
部署后验证 Checklist
▼mermaid复制代码graph TD A["部署完成"] --> B{"浏览器访问<br/>http://服务器IP"} B -->|能看到页面| C{"点击页面内的链接<br/>能正常跳转?"} B -->|白屏/404| D["检查 Nginx 错误日志<br/>sudo tail -f /var/log/nginx/error.log"] C -->|能跳转| E{"刷新浏览器<br/>页面是否正常?"} C -->|404| F["检查 try_files 配置<br/>是不是漏写了 /index.html"] E -->|正常| G["✅ 部署成功!"] E -->|404| H["检查 Nginx 配置<br/>try_files 是否正确"] D --> I["根据错误日志排查"]
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 访问 IP 白屏 | Nginx 没启动 / root 路径写错 | sudo systemctl status nginx 检查状态 |
| 刷新页面 404 | try_files 配置缺失或写错 | 确认 location / 块里有 try_files $uri $uri/ /index.html; |
| 样式错乱/JS 报错 | dist/ 上传不完整 | 重新 scp -r dist/* 上传,或服务器上重新 npm run build |
| 能访问但很慢 | 没开 Gzip / 图片没压缩 | 检查 gzip on; 是否配置,用浏览器 DevTools Network 面板看传输大小 |
4.4 方式二:Docker 容器化部署(推荐生产环境)
这种方式本质是什么?
用 Docker 跑一个 Nginx 容器,把前端打包好的 HTML/CSS/JS 文件"塞"进去,让容器里的 Nginx 对外提供服务。
和"方式一"的核心区别:
- 方式一:Nginx 直接装在服务器上,dist 文件放在服务器的目录里
- 方式二:Nginx 跑在 Docker 容器里,dist 文件要么"打包进镜像",要么"挂载到容器里"
两种子方案对比:
| 子方案 | 做法 | 适用场景 |
|---|---|---|
| A. 打包进镜像(推荐) | dist/ 在构建镜像时就复制进去 | CI/CD 自动化部署,版本可追溯 |
| B. 挂载数据卷 | 容器启动后,把宿主机 dist/ 挂载到容器里 | 快速调试,改了代码不用重新构建镜像 |
子方案 A:打包进镜像(生产推荐)
完整 Dockerfile(逐行大白话注释):
▼dockerfile复制代码# ============ 阶段1:构建前端代码 ============ # 用一个装好 Node.js 的"厨房"来构建项目 FROM node:20-alpine AS builder WORKDIR /app # 先复制 package*.json,再 npm ci # 目的:利用 Docker 缓存,package.json 没变就不重新下载依赖 COPY package*.json ./ RUN npm ci --registry=https://registry.npmmirror.com # 复制所有源代码,然后构建 COPY . . RUN npm run build # 构建完成后,产物在 /app/dist 目录 # ============ 阶段2:用 Nginx 托管 ============ # 换一个"干净盘子":只装 Nginx,不装 Node.js(镜像更小) FROM nginx:1.25-alpine # 删除 Nginx 默认配置(我们不想要那个 "Welcome to nginx!" 页面) RUN rm /etc/nginx/conf.d/default.conf # 把我们写好的 Nginx 配置文件复制进去 # nginx.conf 需要和 Dockerfile 在同一个目录 COPY nginx.conf /etc/nginx/conf.d/default.conf # 把阶段1构建好的 dist/ 复制进 Nginx 的默认网站目录 # /usr/share/nginx/html 是 Nginx 容器的默认 root 路径 COPY --from=builder /app/dist /usr/share/nginx/html # 暴露 80 端口(这只是"文档说明",真正映射端口在 docker run 时指定) EXPOSE 80 # 启动 Nginx(daemon off 表示"前台运行",Docker 容器需要一个前台进程才不会退出) CMD ["nginx", "-g", "daemon off;"]
配套的 Nginx 配置文件(nginx.conf):
▼nginx复制代码# 这个文件和"方式一"的 Nginx 配置几乎一样 # 唯一的区别:root 路径变成了容器内的 /usr/share/nginx/html server { listen 80; server_name _; # _ 表示匹配所有域名(容器内不需要特定域名) root /usr/share/nginx/html; index index.html; # SPA 路由支持(和方式一完全一样,这个必须配!) location / { try_files $uri $uri/ /index.html; } # 静态资源长缓存 location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; } # index.html 不缓存 location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; } # Gzip 压缩 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml; gzip_min_length 1024; }
构建和运行命令:
▼bash复制代码# 构建镜像(在 Dockerfile 所在目录执行) docker build -t myapp-web:v1.0.0 . # 运行容器 docker run -d \ --name myapp-web \ -p 80:80 \ # 把容器的 80 端口映射到宿主机的 80 端口 --restart unless-stopped \ # 容器挂了自动重启 myapp-web:v1.0.0 # 查看日志(确认 Nginx 正常启动) docker logs -f myapp-web
子方案 B:挂载数据卷(调试/快速迭代用)
场景:你在调试前端样式,每次改一点都要重新 docker build,太慢了!
解决:把宿主机的 dist/ 目录"挂载"到容器里,改完代码重新构建后,刷新浏览器就能看到效果,不用重建镜像。
目录结构规划:
▼text复制代码/opt/myapp/frontend/ ← 前端项目根目录 ├── dist/ ← 构建产物(npm run build 生成) ├── nginx.conf ← Nginx 配置文件 └── docker-compose.yml ← 容器编排文件
docker-compose.yml(关键配置说明):
▼yaml复制代码version: '3.8' services: frontend: image: nginx:1.25-alpine container_name: myapp-frontend restart: always # ports:端口映射,格式"宿主机端口:容器端口" # 访问 http://服务器IP:8080 就能看到前端页面 ports: - "8080:80" # volumes(数据卷挂载):这是本方案的核心! # 格式:"宿主机路径:容器路径[:权限]" volumes: # 挂载1:把宿主机 dist/ 挂到 Nginx 的网站目录 # 这样 dist/ 里的文件变了,容器里立刻生效(不用重启容器) - ./dist:/usr/share/nginx/html:ro # ↑ :ro = read-only 只读 # 防止容器内的进程意外修改你的文件 # 挂载2:把宿主机的 nginx.conf 挂进去 # 这样你改了 nginx.conf,执行 nginx -s reload 就能生效 - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro # command:容器启动后执行的命令 # nginx -g "daemon off;" 是前台运行(Docker 容器必须前台运行) command: nginx -g "daemon off;"
完整工作流(大白话版):
▼mermaid复制代码graph LR A["你在本地改代码<br/>保存文件"] --> B["npm run build<br/>重新构建"] B --> C["把 dist/ 同步到服务器<br/>scp -r dist/* user@ip:/opt/myapp/frontend/dist/"] C --> D["刷新浏览器<br/>立刻看到最新效果"] D -->|"Nginx 配置要改?"| E["修改服务器上的<br/>/opt/myapp/frontend/nginx.conf"] E --> F["docker exec myapp-frontend<br/>nginx -s reload"] F --> D
关键命令速查:
| 命令 | 大白话解释 |
|---|---|
docker-compose up -d | "启动容器",-d 是后台运行 |
docker-compose down | "停掉并删除容器",卷挂载的数据不会丢 |
docker exec myapp-frontend nginx -s reload | "让 Nginx 重新加载配置",修改 nginx.conf 后必执行 |
docker logs -f myapp-frontend | "看容器日志",Nginx 报错信息在这里 |
两种子方案怎么选?
▼text复制代码你在做 ↓ 开发调试 / 快速迭代 │ ▼ 用【子方案 B:挂载数据卷】 → 改代码 → build → 刷新浏览器,秒级见效 → 改 Nginx 配置 → docker exec ... nginx -s reload,秒级见效 │ 发版上线 / CI/CD 自动化 │ ▼ 用【子方案 A:打包进镜像】 → docker build 构建镜像 → docker push 推到镜像仓库 → 服务器 docker pull 拉取 → docker run 启动 → 版本可追溯(v1.0.0、v1.0.1...),出问题秒回滚
4.5 环境变量处理技巧
前端项目在构建时注入环境变量,而非运行时:
▼bash复制代码# Vue / Vite 项目:使用 .env 文件 # .env.production VITE_API_BASE_URL=https://api.example.com VITE_APP_TITLE=My App # React / CRA 项目 # .env.production REACT_APP_API_URL=https://api.example.com
▼dockerfile复制代码# Dockerfile 中注入构建时变量 ARG VITE_API_BASE_URL=https://api.example.com ENV VITE_API_BASE_URL=$VITE_API_BASE_URL RUN npm run build
▼bash复制代码# 构建时动态指定 docker build --build-arg VITE_API_BASE_URL=https://api.prod.com -t myapp-web .
注意:如果需要运行时动态修改 API 地址,可在
index.html中注入window.__CONFIG__,通过 Nginx 的sub_filter在启动时替换。
五、Nginx 反向代理与域名配置

5.1 反向代理:统一入口
将前端 + 后端 API 统一到同一域名下,避免跨域问题:
▼nginx复制代码# /etc/nginx/conf.d/myapp.conf # HTTP -> HTTPS 重定向 server { listen 80; server_name www.example.com; return 301 https://$host$request_uri; } # HTTPS 主配置 server { listen 443 ssl http2; server_name www.example.com; # SSL 证书(Let's Encrypt 免费获取) ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; # ---- 前端静态文件 ---- location / { root /var/www/myapp; index index.html; try_files $uri $uri/ /index.html; } # ---- 后端 API 代理 ---- location /api/ { proxy_pass http://127.0.0.1:8080/; # Spring Boot 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; # WebSocket 支持 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; # 超时设置 proxy_connect_timeout 60s; proxy_read_timeout 120s; proxy_send_timeout 60s; } # ---- Python API 代理(如有) ---- location /pyapi/ { proxy_pass http://127.0.0.1:8000/; 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 /assets/ { expires 1y; add_header Cache-Control "public, immutable"; } }
5.2 SSL 证书配置(Let's Encrypt)
▼bash复制代码# 安装 Certbot sudo yum install -y certbot python3-certbot-nginx # 获取证书(自动修改 Nginx 配置) sudo certbot --nginx -d www.example.com # 自动续期(Certbot 会自动添加 cron) sudo certbot renew --dry-run
六、Docker Compose 一键编排

当项目包含前端、后端、数据库等多个服务时,用 Docker Compose 统一管理。
6.1 完整的 docker-compose.yml
▼yaml复制代码# docker-compose.yml version: "3.9" services: # ---- Nginx 网关 ---- nginx: image: nginx:1.25-alpine ports: - "80:80" - "443:443" volumes: - ./nginx/conf.d:/etc/nginx/conf.d - ./nginx/ssl:/etc/letsencrypt - frontend-dist:/usr/share/nginx/html depends_on: - java-api - python-api restart: unless-stopped networks: - app-network # ---- Java Spring Boot 后端 ---- java-api: build: context: ./java-backend dockerfile: Dockerfile environment: - SPRING_PROFILES_ACTIVE=prod - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mydb - SPRING_DATASOURCE_USERNAME=${DB_USER} - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD} - TZ=Asia/Shanghai ports: - "8080:8080" # 仅调试用,生产可去掉 depends_on: db: condition: service_healthy restart: unless-stopped networks: - app-network # ---- Python FastAPI 后端 ---- python-api: build: context: ./python-backend dockerfile: Dockerfile environment: - DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/mydb - TZ=Asia/Shanghai ports: - "8000:8000" # 仅调试用 depends_on: db: condition: service_healthy restart: unless-stopped networks: - app-network # ---- PostgreSQL 数据库 ---- db: image: postgres:16-alpine environment: - POSTGRES_DB=mydb - POSTGRES_USER=${DB_USER} - POSTGRES_PASSWORD=${DB_PASSWORD} - TZ=Asia/Shanghai volumes: - pgdata:/var/lib/postgresql/data # 不暴露端口到公网! # ports: # - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped networks: - app-network # ---- Redis 缓存 ---- redis: image: redis:7-alpine command: redis-server --requirepass ${REDIS_PASSWORD} volumes: - redisdata:/data restart: unless-stopped networks: - app-network volumes: pgdata: redisdata: frontend-dist: networks: app-network: driver: bridge
6.2 环境变量管理
▼bash复制代码# .env 文件(不要提交到 Git!) DB_USER=myuser DB_PASSWORD=your_strong_password_here REDIS_PASSWORD=your_redis_password_here
▼bash复制代码# .gitignore 中添加 .env
6.3 一键启停
▼bash复制代码# 启动所有服务 docker-compose up -d # 查看运行状态 docker-compose ps # 查看日志 docker-compose logs -f java-api # 重启单个服务 docker-compose restart java-api # 停止并删除容器(数据卷保留) docker-compose down # 重新构建并启动 docker-compose up -d --build
七、CI/CD 自动化部署

7.1 GitHub Actions 示例
Java Spring Boot + Docker 自动部署:
▼yaml复制代码# .github/workflows/deploy-java.yml name: Deploy Java API on: push: branches: [main] paths: - 'java-backend/**' jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - name: Build with Maven run: | cd java-backend mvn clean package -DskipTests - name: Build Docker Image run: | cd java-backend docker build -t myapp-java:${{ github.sha }} . - name: Deploy to Server uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /opt/myapp docker pull myregistry/myapp-java:${{ github.sha }} docker-compose up -d java-api docker image prune -f
前端 Vue/React 自动部署:
▼yaml复制代码# .github/workflows/deploy-frontend.yml name: Deploy Frontend on: push: branches: [main] paths: - 'frontend/**' jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' cache-dependency-path: frontend/package-lock.json - name: Install & Build run: | cd frontend npm ci npm run build - name: Deploy to Server uses: appleboy/scp-action@v0.1.7 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} source: "frontend/dist/*" target: "/var/www/myapp" strip_components: 2 - name: Reload Nginx uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: sudo nginx -s reload
7.2 GitLab CI 示例
▼yaml复制代码# .gitlab-ci.yml stages: - build - deploy variables: DOCKER_IMAGE: registry.example.com/myapp build-java: stage: build image: maven:3.9-eclipse-temurin-17 script: - cd java-backend - mvn clean package -DskipTests - docker build -t $DOCKER_IMAGE/java:$CI_COMMIT_SHA . - docker push $DOCKER_IMAGE/java:$CI_COMMIT_SHA only: changes: - java-backend/**/* deploy: stage: deploy script: - ssh deploy@server "cd /opt/myapp && docker-compose pull && docker-compose up -d" only: - main when: manual # 需手动触发部署
八、生产环境最佳实践

8.1 安全清单
- 不使用 root 运行应用:Docker 容器内创建专用用户
- 数据库不暴露公网:通过安全组/防火墙限制为内网访问
- 敏感信息使用环境变量:密码、密钥不硬编码,不提交 Git
- 启用 HTTPS:Let's Encrypt 免费 SSL
- 设置安全响应头:X-Frame-Options、CSP、HSTS
- 定期更新依赖:关注安全漏洞公告
- SSH 密钥登录:禁用密码登录
8.2 日志管理
▼bash复制代码# Docker 日志限制 # /etc/docker/daemon.json { "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" } }
▼bash复制代码# Spring Boot 日志配置(application-prod.yml) logging: file: name: /app/logs/application.log logback: rollingpolicy: max-file-size: 50MB max-history: 30 total-size-cap: 1GB
8.3 监控告警
| 工具 | 用途 | 推荐场景 |
|---|---|---|
| Prometheus + Grafana | 指标采集与可视化 | 中大型项目 |
| Uptime Kuma | 站点可用性监控 | 个人/小团队 |
| Sentry | 错误追踪 | 所有项目 |
| Portainer | Docker 可视化管理 | 容器化项目 |
8.4 备份策略
▼bash复制代码#!/bin/bash # backup.sh - 数据库定时备份脚本 DATE=$(date +%Y%m%d_%H%M%S) BACKUP_DIR="/opt/backups" # PostgreSQL 备份 docker exec db pg_dump -U myuser mydb | gzip > "$BACKUP_DIR/db_$DATE.sql.gz" # 保留最近 7 天备份 find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete echo "Backup completed: db_$DATE.sql.gz"
▼bash复制代码# 添加 crontab 定时任务(每天凌晨 3 点执行) crontab -e 0 3 * * * /opt/scripts/backup.sh >> /opt/backups/backup.log 2>&1
8.5 性能优化速查
| 项目类型 | 优化点 | 具体措施 |
|---|---|---|
| Java | JVM 调优 | G1GC、容器感知内存、连接池 |
| Python | 并发模型 | Gunicorn 多 worker、异步 IO |
| 前端 | 资源优化 | Gzip/Brotli 压缩、CDN、懒加载 |
| Nginx | 连接优化 | keepalive、upstream 缓存、限流 |
| 数据库 | 查询优化 | 索引、连接池、读写分离 |
九、常见问题排查
9.1 Java 项目
| 问题 | 原因 | 解决方案 |
|---|---|---|
OutOfMemoryError | 堆内存不足 | 调大 -Xmx,查看 logs/heapdump.hprof 分析内存 |
| 启动慢 | 依赖多、扫描路径大 | 开启懒加载 spring.main.lazy-initialization=true |
| 连接数据库超时 | 网络或连接池配置 | 检查安全组,调整 spring.datasource.hikari |
| Docker 容器时区不对 | 默认 UTC | 设置 TZ=Asia/Shanghai |
| 脚本 stop 后进程仍在 | 优雅停机超时 | 检查应用是否注册了 shutdown hook,脚本会在 30 秒后自动 kill -9 |
| 外部配置不生效 | --spring.config.location 路径错误 | 确认以 / 结尾,如 ../config/,文件名须为 application-prod.yml |
| 脚本提示 JAR not found | JAR 文件名与脚本配置不一致 | 检查 app.sh 中的 JAR_NAME 变量是否与实际文件名匹配 |
| 日志目录不存在 | 首次部署未创建目录 | 脚本会自动创建;如手动启动需 mkdir -p logs tmp |
kill -9 后端口仍占用 | 进程残留 | 等待 1-2 分钟释放,或 ss -tlnp | grep :8080 检查 |
| 回滚后配置不匹配 | 备份的 JAR 与现有 config 不兼容 | 同时备份 config 目录,或确保配置向后兼容 |
9.2 Python 项目
| 问题 | 原因 | 解决方案 |
|---|---|---|
ModuleNotFoundError | 依赖未安装 | 检查 requirements.txt,确认 venv 激活 |
| 502 Bad Gateway | Gunicorn 挂了 | 查看日志 docker logs,增加 worker/超时 |
| 静态文件 404 | 路径配置错误 | 检查 STATIC_URL 和 Nginx alias |
| 并发上不去 | GIL 限制 | 增加 worker 数,考虑异步框架(FastAPI) |
9.3 前端项目
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 刷新页面 404 | Nginx 未配置 SPA 回退 | 添加 try_files $uri $uri/ /index.html |
| 接口跨域 | 前后端不同端口/域名 | Nginx 反向代理统一域名 |
| 更新后白屏 | 浏览器缓存旧文件 | index.html 设 no-cache,静态资源加 hash |
| 环境变量不生效 | Vite/CRA 构建时注入 | 检查 .env.production,确认 VITE_/REACT_APP_ 前缀 |
| Docker 镜像太大 | 未用多阶段构建 | 使用 builder 阶段,最终镜像仅含 Nginx + 静态文件 |
9.4 通用排查命令
▼bash复制代码# 检查端口占用 ss -tlnp | grep :8080 # 检查 Docker 容器状态 docker ps -a # 查看容器日志(最后 100 行) docker logs --tail 100 -f myapp # 检查 Nginx 配置语法 sudo nginx -t # 测试接口连通性 curl -v http://localhost:8080/actuator/health curl -v http://localhost:8000/health # 查看磁盘空间 df -h # 查看内存使用 free -h # 查看系统负载 uptime
快速部署速查表
| 技术栈 | 构建命令 | 部署方式 | 默认端口 |
|---|---|---|---|
| Spring Boot | mvn clean package | 脚本 / Docker Compose / Swarm | 8080 |
| FastAPI | - | Gunicorn + Uvicorn / Docker | 8000 |
| Flask | - | Gunicorn / Docker | 8000 |
| Vue (Vite) | npm run build | Nginx / Docker | 80 |
| React (Vite/CRA) | npm run build | Nginx / Docker | 80 |
写在最后:部署没有银弹,适合自己的才是最好的。小型项目用 JAR/venv + systemd 就够了,中大型项目上 Docker Compose + CI/CD,微服务架构考虑 Kubernetes。先跑起来,再优化。遇到问题别慌,看日志,查端口,99% 的问题都能定位。
