干开发这些年,最常见的场景就是:代码写得挺欢,一到“部署”这两个字就头疼。Java项目打包出个jar或者war,扔到服务器上跑不起来;Python项目本地运行没问题,换台机器一堆依赖报错。尤其是从“能运行”到“稳定运行”这中间那一大段路——环境配置、进程守护、日志排查、端口占用——踩的坑一个比一个经典。
这篇就来聊聊Java和Python开发项目的部署这件事。不是那种照着文档念一遍的官方教程,而是把我实际部署过程中验证过的方案、踩过的坑、调优过的配置都拿出来,从服务器环境准备到Java和Python各自的主流部署方式,再到前后端分离项目的整体落地,一步一步拆开讲。无论你是在本地Windows/Linux上做开发调试,还是要把项目真正扔到云服务器上给用户用,这篇都能给你一套可以照着抄的完整参考。
1. 部署的整体思路与环境准备
部署这件事,本质上就是“把代码变成服务”的过程。代码在你电脑上能跑,不代表它在服务器上也能跑——依赖、端口、内存、权限、日志,任何一环出问题,服务就起不来。所以先把整体思路理清楚,比急着敲命令重要得多。
1.1 先搞清楚项目到底需要什么东西
拿到一个项目,别急着装环境。我的习惯是先用几分钟把项目的“家底”摸清楚,列一张清单:
- 运行时环境:Java项目需要JDK,Python项目需要Python解释器,版本对不对直接影响能不能跑。
- 依赖管理:Java项目用Maven还是Gradle,Python项目用pip还是conda,依赖锁文件在哪。
- 服务形态:是单体应用(Spring Boot fat jar、Flask app),还是需要Web服务器容器(Tomcat部署war包、Nginx+uWSGI跑Django)。
- 外部依赖:数据库用什么(MySQL、PostgreSQL、Redis)、有没有消息队列、对象存储,这决定了部署前要不要先准备这些服务。
- 端口规划:项目监听什么端口,谁来转发外部请求,防火墙规则怎么放行。
举个例子。一个典型的Spring Boot项目,pom.xml里dependencyManagement指定了Spring Boot 2.7.x,JDK至少得17;一个Django项目,requirements.txt里写着Django 4.2,Python得3.10往上,数据库如果是PostgreSQL还得装psycopg2-binary。这些信息都会直接影响部署方案的选择。
1.2 服务器准备与目录规划建议
服务器这块,新手最容易犯的错就是“环境装得乱七八糟”。今天装个JDK到默认路径,明天装个Python到/user/local,时间一长自己都找不到东西在哪。我的建议是建立一个统一的目录规范:
/opt/ ├── projects/ # 项目代码统一放这 ├── runtime/ # 运行时(JDK、Python虚拟环境等) ├── logs/ # 日志统一目录 └── backup/ # 备份目录即使你只有一台小内存云服务器,也建议按这个思路分目录,别把项目直接铺在根目录或home下。原因很实际:部署的排查效率,大部分靠“东西在哪”撑起来。日志统一了,排查看/opt/logs;代码统一了,改配置不用到处找。
Linux服务器上建议使用普通用户(比如叫deploy)运行服务,不要用root直接跑应用。原因有两个:一是安全问题,应用进程如果被攻击,root权限的破坏力更大;二是误操作风险,用root改错文件的代价实在太高。需要权限的端口(比如80、443)通过Nginx转发或setcap解决,后面会细说。
2. Java项目的部署实战
Java项目中,目前最常见的形态就三种:传统的war包扔Tomcat、Spring Boot的fat jar直接java -jar、以及容器化docker run。下面把每种的流程、关键配置和坑都过一遍。
2.1 JDK的安装与版本选择技巧
JDK是Java项目的命根子。现在JDK版本很多,但实际部署时我的选择逻辑很简单:
- Spring Boot 2.x 项目:JDK 8或11。虽然JDK 8已经过了免费商用期,但大量老项目还在用,除非有安全合规要求,否则稳定压倒一切。
- Spring Boot 3.x 项目:JDK 17起步,这是Spring官方指定的基础版本。
- 纯Java工具类项目:JRE都行,但建议装JDK,因为后续可能要用jstack、jmap等排查工具。
安装方式推荐用包管理器或二进制解压,不要用IDE自带的JDK。CentOS/RHEL系用yum,Ubuntu/Debian系用apt,能少很多环境变量配置的麻烦。如果是自己手动解压JDK,一定要记得配JAVA_HOME:
# 以OpenJDK 17为例,解压到/opt/runtime/jdk-17后 export JAVA_HOME=/opt/runtime/jdk-17 export PATH=$JAVA_HOME/bin:$PATH配置写进/etc/profile.d/java.sh,再source /etc/profile,这样ssh新开的会话也能生效。一个非常常见的坑是:明明系统里装了多个JDK,java -version显示的却不是你期望的那一个。排查思路很简单,which java看路径,再echo $JAVA_HOME看环境变量,不对就在.bashrc或profile.d里修正。
2.2 构建工具——Maven的项目打包细节
Java项目构建,现在还是Maven占绝对大头,Gradle在Android和部分新项目里用得多。构建这一环,最核心的要务是“可复现构建”。
以Maven为例,项目中一定要有pom.xml,并且锁定parent版本和依赖版本。打包命令很简单:
mvn clean package -DskipTests但有几点值得注意:
-DskipTests是跳过测试代码的编译吗?不是。它跳过的是测试执行,但测试代码还是会被编译。如果想连编译都跳过,用-Dmaven.test.skip=true。实际部署时我一般用-DskipTests,毕竟测试代码编译一下也能提前暴露问题。- Maven默认会在本地仓库缓存依赖,
~/.m2/repository,第一次打包会下很多依赖,时间久是正常的。如果想在服务器上打包,务必要把国内镜像配好,比如阿里云镜像,否则下载速度能让人怀疑人生。 - 打war包还是jar包,取决于
pom.xml中<packaging>标签(jar或war),以及Spring Boot的spring-boot-maven-plugin是否是内置容器。如果用Tomcat部署war,Spring Boot项目还要额外继承spring-boot-starter-tomcat并将主类配置为SpringBootServletInitializer子类,否则war扔进Tomcat会404。
打包完,artifact在target目录下。常见命名格式:项目名-版本号.jar或.war。这一步产出才是要部署的产物。
2.3 Tomcat部署war包的完整流程
Tomcat部署war,多见于传统JavaWeb项目,比如电商系统(tpshop这类)、老管理系统。流程不复杂,但坑不少。
首先去Apache Tomcat官网下载对应版本的二进制包,解压到/opt/runtime/tomcat9。然后需要修改的核心配置是conf/server.xml:
- HTTP端口:默认8080,如果和现有服务冲突,改成你规划的端口。
appBase:默认webapps目录。war包扔进去,Tomcat启动时会自动解压。- 最大线程数配置(这也是性能调优的关键):
<Connector port="8080" protocol="HTTP/1.1" maxThreads="200" minSpareThreads="20" connectionTimeout="20000" redirectPort="8443" />maxThreads数量设置的逻辑是:不要简单照搬网上推荐的200/400/800,而要按你服务器CPU核数和业务特性评估。一个CPU密集型的服务,线程数远大于CPU核数意义不大,反而增加上下文切换开销。一般起步按CPU核数的2-4倍设置,再压测调整。
部署war时有个非常细节的操作:如果war包重名,Tomcat会保留旧解压目录。你想强行更新,建议先把旧war和解压目录都删掉,再把新war放进去。我见过太多“明明替换了war,访问还是旧页面”的问题,最后发现是旧目录残留。
Tomcat启动脚本是bin/startup.sh,停止是shutdown.sh。注意startup.sh启动后是后台进程,要看日志去logs/catalina.out。这里有个强劲推荐:启动后一定马上tail一下日志,确认没有异常再关闭会话。不要启动完直接走人,等三分钟后一堆连接异常再回来查,效率太低了。
2.4 Spring Boot的jar包部署与常用参数
Spring Boot打出来的fat jar,是现在Java项目部署的主流形态,因为它自带Tomcat,不需要额外装应用服务器,部署就是从一条命令开始的:
java -jar app.jar但是直接裸奔运行有它的局限,至少要加这么几个固定参数:
nohup java -Xms512m -Xmx512m -XX:+UseG1GC \ -jar /opt/projects/app.jar \ --server.port=8080 \ --spring.profiles.active=prod \ > /opt/logs/app.out 2>&1 &几个参数说明:
-Xms512m -Xmx512m:初始堆和最大堆设成一样,避免运行时动态扩容导致GC频繁。具体值按服务器内存和项目要求来,多数业务系统512m-2g比较常见。--spring.profiles.active=prod:指定生产环境配置。这句话能救命的场景是本地配置和线上配置不同步,把database地址、redis地址全写在application-prod.yml里,部署时用profile隔离,改环境不用改代码。nohup ... &:后台运行。注意一定要把标准输出和错误输出重定向到日志文件,否则用nohup跑久了日志会堆积到nohup.out里不说,还可能因为你关闭终端导致输出中断。> /opt/logs/app.out 2>&1:把out和err都送到同一个文件。Java项目一个特有坑:System.out输出的日志,如果不重定向,报关会话后就找不到了。
如果项目里用了外置的配置文件,更推荐的做法是配合--spring.config.location显式指定:
java -jar app.jar --spring.config.location=/opt/projects/app/config/application.yml这样改配置不用重新打包,部署灵活性高很多。
2.5 用Docker部署Spring Boot项目
容器化部署这几年已经成为主流,特别是Spring Boot这种“自带容器”的Java项目,做成Docker镜像非常自然。一个最简Dockerfile:
# 构建阶段 FROM maven:3.8-openjdk-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src . RUN mvn clean package -DskipTests # 运行阶段 FROM openjdk:17-jre-slim WORKDIR /app COPY --from=builder /build/target/app.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-Xms512m", "-Xmx512m", "-jar", "app.jar"]有几个细节值得说道说道:
- 多阶段构建的目的是让最终镜像只包含运行环境(JRE),不携带Maven和源码,镜像体积小几个级别。能看到只有
openjdk:17-jre-slim加一个jar,运行阶段少了很多没用的东西。 RUN mvn dependency:go-offline是把依赖先下载好,这样后续改代码打包能有缓存,构建速度快很多。第一次构建没有缓存,得耐心等。- 镜像跑起来时,端口映射要注意:
docker run -d -p 8080:8080 --name app myapp:latest。左边是宿主机端口,右边是容器端口。如果你在容器里改server.port=8081,右端口也要跟着改。
Docker方式最大的价值是环境一致性。同一个镜像在开发机、测试机、生产机上跑出来的结果完全一样,再也不用面对“我本地好的呀”这种灵魂拷问。
3. Python项目的部署实战
Python项目的部署和Java非常不同:Java构建出来的jar/war本身是编译产物,而Python是解释型语言,部署的本质是“把源码和运行环境一起搬运过去”。这也意味着Python部署的核心痛点集中在Python版本管理、依赖隔离、进程管理这三件事上。
3.1 Python解释器安装与虚拟环境配置
服务器上预装的Python版本通常比较老(CentOS 7自带Python 2.7,Ubuntu 20.04是Python 3.8),而现在好多项目要求Python 3.9、3.10甚至3.11。这时候就需要手动安装Python。
推荐用源码编译安装,可控性最强:
# 下载Python 3.10.12 wget https://www.python.org/ftp/python/3.10.12/Python-3.10.12.tgz tar zxvf Python-3.10.12.tgz cd Python-3.10.12 ./configure --prefix=/opt/runtime/python3.10 --enable-optimizations make -j$(nproc) make install--prefix指定安装目录, 不污染系统自带的Python。--enable-optimizations会启用PGO优化,编译时间长一些,但运行时性能更好。装完确认一下:
/opt/runtime/python3.10/bin/python3.10 -V接下来是虚拟环境。Python项目依赖隔离这件事,我强调多少次都不算多。千万别“图省事”把所有项目的依赖都装进系统Python,否则过俩月装一个新项目时,依赖版本冲突能让你崩溃。我们团队的统一做法是:每个项目一个venv。
创建虚拟环境用自带的venv模块:
cd /opt/projects/myproject /opt/runtime/python3.10/bin/python3.10 -m venv venv source venv/bin/activate pip install -r requirements.txt激活后which pip要显示虚拟环境内的pip路径。如果发现不在,检查是不是虚拟环境没激活,或者shell缓存问题,用hash -r刷新。装了依赖后,务必用一个简单命令验证:
python -c "import flask, requests; print('deps ok')"这一步能在部署阶段提前暴露依赖缺失,远比服务启动时报ModuleNotFoundError快。
3.2 依赖管理的几种主流方案对比
Python依赖管理,目前有pip + requirements.txt、Pipenv、Poetry这三种流派。
- requirements.txt是老牌可靠方案,适合大多数中小项目。写法上推荐用
pip freeze生成锁定版本(会带上所有间接依赖,版本号完全锁定),不要只写顶级依赖名如requests不带版本——那等到两年后部署,pip可能装了一个不兼容的新版本。 - Pipenv用Pipfile和Pipfile.lock管理,引入了“锁定文件”的概念,对虚拟环境下管理直观,但历史包袱比较多,社区活跃度有所下降。
- Poetry是现在个人比较推荐的方案,pyproject.toml集中管理项目元数据和依赖,一个文件干完很多事,锁文件精确锁定依赖树。就是学习曲线比pip稍微陡一点。
我的建议很直接:中小项目用pip freeze > requirements.txt + venv,地够用,团队成员都会;新项目想走得远一点,直接上Poetry,以后迁移、升级都顺手。但不管哪种方案,锁定版本是硬道理。
3.3 用Gunicorn跑Flask/Django应用
Python项目跑起来之后,直接暴露给公网是不建议的,哪怕是开发环境。常规做法是用Gunicorn(Green Unicorn)作为WSGI服务器,跑Python应用,再用Nginx做反向代理。
安装Gunicorn:
pip install gunicorn以Flask为例(假设启动文件是app.py,应用实例叫app),启动命令:
gunicorn -w 4 -b 0.0.0.0:8000 app:app --daemon-w 4:启动4个worker进程,这个数值一般按CPU核心数*2+1来估。典型2核服务器设5个左右,4核设9个,但也不是越大越好,要按你的服务负载实测调整。-b 0.0.0.0:8000:绑定所有网卡的8000端口。很多人问为什么不是127.0.0.1——如果服务只给本机Nginx访问,用127.0.0.1更安全;但如果想直接通过公网IP访问测试,就得0.0.0.0。--daemon:后台运行。但我个人更推荐不考虑daemon,而是配合systemd或supervisor来管理进程,这样能自动重启、随系统启动、崩溃被拉起。
Django的话,Gunicorn命令写法类似:
gunicorn myproject.wsgi:application -w 4 -b 127.0.0.1:8000注意要切到项目目录下运行,Django的配置文件路径才能解析到。
Gunicorn有个很有用的参数:--timeout。默认30秒,如果某个接口耗时超过30秒被kill掉,接口会返回502。大数据导出、报表生成这类接口,就需要调大:
gunicorn -w 4 -b 127.0.0.1:8000 app:app --timeout 1203.4 用Nginx为Python项目做反向代理
Nginx在前面承接外部HTTP请求,把动态请求转发给Gunicorn,这是Python生产环境部署最经典的一层架构。Nginx做两层事情:暴露80/443端口,和托管静态文件。
一个最简配置:
server { listen 80; server_name example.com; # 静态文件由nginx直接返回,不经过gunicorn location /static/ { alias /opt/projects/myproject/static/; expires 30d; } location / { 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; } }location /static/这块对性能提升非常明显。Python应用如果用框架直接返回静态文件,每个请求都要过一遍WSGI、Python代码,Nginx直接读磁盘文件返回快几个数量级。Django项目的static文件需要先执行python manage.py collectstatic收拢到一个目录。
Nginx改完配置后,先nginx -t检查语法,确认没问题再nginx -s reload平滑重载。不要直接nginx -s stop再启动,能避免一瞬间的服务中断。
3.5 Python项目的systemd服务单元配置
手工nohup + Gunicorn的daemon模式有一个痛点:服务器重启后服务不会自己恢复,进程崩了也没有自动拉起。解决这个问题的最佳方式是用systemd写服务单元。
下面是Flask项目一个完整的service文件:
[Unit] Description=My Flask App After=network.target [Service] User=deploy Group=deploy WorkingDirectory=/opt/projects/myproject Environment="PATH=/opt/projects/myproject/venv/bin" Environment="PYTHONUNBUFFERED=1" ExecStart=/opt/projects/myproject/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 app:app Restart=always RestartSec=3 [Install] WantedBy=multi-user.target几个字段的讲究:
Environment="PATH=...":让Gunicorn用虚拟环境内的Python,避免依赖解析到系统Python的site-packages。PYTHONUNBUFFERED=1:让Python输出不缓冲,日志实时写入,否则journald里可能捞不到最新日志。Restart=always:进程不管什么原因退出(崩了、被kill),systemd都会在3秒后自动重拉。WorkingDirectory:Gunicorn的工作目录必须和项目目录一致,否则相对路径加载配置会失败。
保存到/etc/systemd/system/myapp.service后,执行:
systemctl daemon-reload systemctl enable myapp systemctl start myapp systemctl status myappenable是设置开机自启,status能看到当前状态和最近日志。这套组合拳下来,Python服务的“保活”就交给systemd了,比什么nohup都要可靠一个档次。
4. 前后端分离项目的部署要点
现在很多Java和Python项目都走向了前后端分离:前端Vue/React打包成纯静态文件,后端出RESTful API。这类型的部署有一个共同的“万能钥匙”:用一个Nginx,同时托管前端静态文件、反向代理后端API、解决跨域问题。
4.1 前端静态资源的构建与部署
以最常见的Vue项目为例,构建命令:
npm install npm run build执行完成后会生成dist/目录,里面是index.html、js/css资源等纯静态文件。把dist目录整体上传到服务器的/opt/projects/frontend/dist,就完成一半了。
Nginx托管部分:
server { listen 80; server_name example.com; root /opt/projects/frontend/dist; index index.html; # 前端路由是history模式时,刷新页面会404,需要这个配置 location / { try_files $uri $uri/ /index.html; } }关键就是try_files $uri $uri/ /index.html;这一行。Vue Router在history模式下,用户访问/user/123刷新时,Nginx找不到这个真实文件,就会直接返回404;try_files把它重写到index.html,让前端路由接管。对SPA有核心作用,少了它大概率出事。
4.2 API反向代理与跨域问题的一并解决
前端和后端如果部署在同域下(即前端访问/api/user,Nginx转发给后端http://127.0.0.1:8080/api/user),那是节省事情的做法,因为同域根本没跨域问题。
Nginx配置:
location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这里有一个特别容易踩的坑:proxy_pass的URL末尾是否带/,行为完全不同。proxy_pass http://127.0.0.1:8080;(不带/)会保留原路径;proxy_pass http://127.0.0.1:8080/;(带/)会把location前缀替换掉。比如请求/api/user,第一种后端收到/api/user;第二种后端收到/user。所以一定要按你后端接口的实际路径设计好要不要带这个斜杠。
如果前后端必须分开域(比如前端部署在CDN,后端单独一个域名),跨域问题就得用后端解决。以Spring Boot为例,最常用的两种方式:
- 加
@CrossOrigin注解在Controller类或方法上。 - 写一个全局CORS配置类。
Python Flask的方式:
from flask_cors import CORS CORS(app)Django则用django-cors-headers中间件。但我的经验是:尽量通过Nginx同域部署来解决跨域,逼不得已再配置CORS。因为CORS配置不当非常容易引发安全隐患,而且排查起来debug难度更高。
4.3 前后端分离项目的部署检查清单
以下是我每次部署前后端分离项目都会过的检查项:
- 前端dist上传后,先直接访问静态资源地址,确认能打开index.html、css、js能加载。
- 直接curl后端API接口,确认接口能返回数据。
- 在浏览器Network面板看请求和后端响应,重点观察有没有404、502、503。
- 前端页面如果白屏,先看Console的报错,多数是因为静态资源路径不对(publicPath配置问题)或API地址不对。
- 后端日志看请求是否到达,判断问题是出在Nginx层还是后端服务。
这份清单能帮你把“前端问题、后端问题、Nginx问题”快速切分,让排查思路清晰很多。
5. 日志管理与问题排查的实战思路
部署完只是开始,真正考验人的是第二天早上运维告警说服务不可用。这部分是我写这篇博文最想让读者拿走的部分——一套完整的日志管理和问题排查思路。
5.1 日志的规划与轮转配置
先说要做到什么程度:日志必须能按天或者按大小切分,不能一个文件写到爆炸。
Java Spring Boot项目,推荐使用logback自带的滚动策略,在logback-spring.xml里配置<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">。但更省心的方案:让应用日志输出到固定目录,再配合Linux的logrotate做系统级轮转。
logrotate的配置示例(/etc/logrotate.d/java-app):
/opt/logs/app.out { daily rotate 7 copytruncate compress missingok notifempty }daily:每天轮转一次。rotate 7:保留7个历史文件。copytruncate:先复制当前日志再截断,这样不需要重启应用。compress:历史日志压缩成.gz,节省磁盘。
如果你用systemd管理Python服务,可以不开自己的日志文件,直接用journalctl -u myapp看日志。但要控制journald的体积,可以在/etc/systemd/journald.conf设置SystemMaxUse=500M,防止日志把磁盘写满。
Python项目用Gunicorn时,访问日志和错误日志也要分开:
--access-logfile /opt/logs/gunicorn-access.log\ --error-logfile /opt/logs/gunicorn-error.log两个日志分开能显著提升排错效率。
5.2 高频问题速查与排查流程
我整理了一个部署阶段最常见的问题速查表,可以说覆盖了80%的情况:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 网络请求返回404 | 路径配错、tomcat/work目录残留、路由模式问题 | 检查Nginx配置、curl看返回、检查前后端路径 |
| 502 Bad Gateway | 后端服务没起来、端口不对、防火墙拦截 | systemctl status看服务状态、ss -tlnp看端口监听、直接curl后端地址 |
| 503 Service Unavailable | Nginx上游配置错误、后端过载未启动 | 看Nginx error.log和上游健康检查配置 |
| Java进程启动即退出 | 端口被占用、配置文件语法错误、内存不足 | ss -tlnp看端口、tail -100 /opt/logs/app.out看具体报错 |
| Python启动报ModuleNotFoundError | 依赖没装全、虚拟环境路径不对 | 重新pip install -r requirements.txt、python -c "import xxx"验证 |
| 页面访问很慢 | 静态文件没走Nginx、数据库慢查询、连接池太小 | 先压测看瓶颈在Nginx还是后端,再针对性调优 |
| 定时任务不执行 | cron环境变量、Python虚拟环境路径没配全 | 检查crontab里的PATH、用绝对路径执行命令 |
排查时我习惯的流程是“从外到内”:
- 先看网络层:
curl -I http://127.0.0.1和curl -I http://公网IP,判断是服务器外网问题还是服务本身问题。 - 再看服务层:
systemctl status 服务名,或ps aux | grep java/python,确认进程还活着。 - 再看日志层:Java项目看catalina.out或app.out,Python项目看gunicorn-error.log或journalctl。
- 最后才深入业务代码级问题。
这个顺序能让你在五分钟内定位绝大多数部署问题。反着来,一上来就看代码,效率极低。
5.3 端口排查与防火墙放行的经验
一个新项目上线,最容易碰到的是“服务起来了,外面访问不了”。原因多半是两个:服务没监听0.0.0.0,或者防火墙没放行端口。
查看端口监听状态:
ss -tlnp输出的Local Address列如果显示127.0.0.1:8080,说明只有本机能访问,外部网络访问不了——需要改成0.0.0.0:8080。如果显示:::8080则是IPv6的通配监听。
防火墙这块,不同发行版命令不一样:
- UFW(Ubuntu):
sudo ufw allow 8080/tcp - firewalld(CentOS 7+):
firewall-cmd --permanent --add-port=8080/tcp && firewall-cmd --reload - 云服务器安全组:这个最容易忽略——即使服务器内部防火墙全开了,云控制台的安全组/防火墙没放行,公网依然进不来。
还记得我刚入行时部署一个Django项目,在服务器上curl死活通,公网访问就是不行,整整查了一小时,最后发现是云控制台安全组忘了加规则。
6. 自动化部署的进阶方向
前面讲的都是手工部署,流程固定了之后,完全可以脚本化、自动化。下面说几个我从手工走向自动化的组合装备,不复杂但实用价值极高。
6.1 编写一键部署脚本
以Java Spring Boot项目为例,一个典型的deploy.sh:
#!/bin/bash # 一键部署脚本 - Spring Boot项目 APP_NAME="app.jar" APP_PATH="/opt/projects/app" BACKUP_PATH="/opt/backup" LOG_PATH="/opt/logs" echo "=== 1. 备份旧版本 ===" if [ -f "$APP_PATH/$APP_NAME" ]; then TIMESTAMP=$(date +%Y%m%d%H%M%S) cp "$APP_PATH/$APP_NAME" "$BACKUP_PATH/${APP_NAME%.jar}-$TIMESTAMP.jar" echo "已备份至 $BACKUP_PATH" fi echo "=== 2. 停掉旧进程 ===" PID=$(pgrep -f "java -jar $APP_PATH/$APP_NAME") if [ -n "$PID" ]; then kill $PID sleep 5 fi echo "=== 3. 拷贝新包 ===" cp /tmp/dist/$APP_NAME $APP_PATH/ echo "=== 4. 启动服务 ===" nohup java -Xms512m -Xmx512m -jar $APP_PATH/$APP_NAME \ --spring.profiles.active=prod \ > $LOG_PATH/app.out 2>&1 & echo "=== 5. 检查启动状态 ===" sleep 10 tail -30 $LOG_PATH/app.out这个脚本你在每次发布时都会用,建议把备份、停服、替换、启动、检查做成一次性的、运行完有明确提示的步骤,比手动敲命令能防住很多手滑事故。Python项目也可以如法炮制,差异只是启动命令换成Gunicorn/systemctl。
6.2 引入Jenkins流水线与部署的衔接
项目发布频率高了,人会越来越懒,于是自动化CI/CD就安排上了。这里只说一个关键点:流水线的产物最好就是最终要运行的那份包/镜像。
不要在每个环境上都重复构建,而是让Jenkins在流水线中完成构建,产出的jar、war或Docker镜像,由流水线直接分发到服务器。像这样的话,测试环境测过的包和生产环境跑起来的包,是完全同一个二进制。
一个简化的Jenkins流水线(Jenkinsfile):
pipeline { agent any stages { stage('拉取代码') { steps { git branch: 'main', url: 'git@github.com:xxx/app.git' } } stage('构建') { steps { sh 'mvn clean package -DskipTests' } } stage('发布') { steps { sh 'scp target/app.jar deploy@server:/opt/projects/app/' sh 'ssh deploy@server "cd /opt/projects/app && ./deploy.sh"' } } } }对于Docker部署的项目,更优雅的做法是构建镜像推到镜像仓库,服务器上用Portainer或Watchtower自动拉取更新。这块涉及的东西比较多,但核心思想是一致的:把重复劳动交给我们写的自动化脚本,把人的精力留在真正需要判断的事情上。
6.3 部署后的健康检查与回滚预案
部署上线后,我强烈建议至少做三件事:
一是立即检查健康检查接口。Spring Boot可以引入spring-boot-starter-actuator,暴露/actuator/health;Flask可以写一个最简单的/health视图返回ok。部署脚本里最后检查这个接口返回200才算部署成功。
二是对数据库做一次备份。特别是涉及表结构变更的版本,没有备份就直接升级,一旦出问题要回滚就是自找麻烦。
三是提前准备好回滚方式。脚本化部署的反面就是回滚:保留上一个版本的备份,脚本加一个--rollback参数,一键恢复到上一个版本的jar或代码。没有回滚预案的发布,叫冒险;有回滚预案的发布,才叫技术操作。
7. 关于部署工作的一些个人体会
老话说得对,“能跑起来不是本事,倒不了才见功夫”。
早年我部署项目也踩坑,最狠的一次半夜发版,把war包覆盖错了位置,Tomcat起不来,数据还差点出事——从那以后我养成一个毛病:任何发布,先备份、再操作、后验证,三步缺一不可。
部署这门手艺,最核心的不是把命令背熟,而是背后的一整套流程意识:哪些参数是真正与你服务器性能相关的,哪些路径是统一规划的,日志在哪查,服务怎么保活,万一挂了怎么回滚。把这些想明白了,Java还是Python,传统服务器还是容器,差异都只是命令层面的小事。
最后再分享一个小技巧:部署脚本里尽量用绝对路径,少用相对路径。因为ssh会话的初始目录你没法保证,一旦脚本用了相对路径,就可能因为目录不对而找不到文件,那种报错排查起来十分让人崩溃。