Java + Vue 项目部署运维 — 纯命令行版
不使用宝塔面板,纯 Linux 命令行完成 Spring Boot + Vue 3 + MySQL + Redis + Nginx 的部署与运维。适合对服务器有完全掌控力、希望理解每一步操作的场景。
一、服务器环境准备
1.1 系统初始化
# 更新系统(CentOS / Ubuntu 选其一)# CentOS 7/8yum update -yyum install -y epel-release vim wget curl git unzip lrzsz net-tools bind-utils# Ubuntu 22.04 / Debian 12apt update && apt upgrade -yapt install -y vim wget curl git unzip lrzsz net-tools dnsutils1.2 创建项目目录结构
# 统一目录规划mkdir -p /www/wwwroot/api # 后端应用mkdir -p /www/wwwroot/api/logs # 后端日志mkdir -p /www/wwwroot/api/backup # 后端备份mkdir -p /www/wwwroot/web # 前端静态文件mkdir -p /www/wwwroot/upload # 上传中转目录mkdir -p /www/backup/mysql # 数据库备份mkdir -p /www/backup/web # 前端备份mkdir -p /www/wwwlogs # 统一日志目录# 设置权限chmod 755 /www/wwwroot1.3 安装 JDK 17
# 下载并解压cd /usr/localwget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gztar -zxvf jdk-17_linux-x64_bin.tar.gzmv jdk-17* jdk17rm -f jdk-17_linux-x64_bin.tar.gz# 配置环境变量cat >> /etc/profile << 'EOF'# JDK 17export JAVA_HOME=/usr/local/jdk17export JRE_HOME=$JAVA_HOMEexport PATH=$JAVA_HOME/bin:$PATHexport CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jarEOFsource /etc/profilejava -version# openjdk version "17.0.x"1.4 安装 MySQL 8
# === CentOS ===# 添加 MySQL 官方源yum localinstall -y https://dev.mysql.com/get/mysql80-community-release-el7-7.noarch.rpmyum install -y mysql-community-server# === Ubuntu ===wget https://dev.mysql.com/get/mysql-apt-config_0.8.25-1_all.debdpkg -i mysql-apt-config_0.8.25-1_all.debapt update && apt install -y mysql-server# 启动并设置开机自启systemctl start mysqldsystemctl enable mysqld# 获取初始密码(CentOS)grep 'temporary password' /var/log/mysqld.log# 输出类似:A temporary password is generated for root@localhost: xxxxxxxx# 登录并修改密码mysql -u root -p-- 修改 root 密码ALTER USER 'root'@'localhost' IDENTIFIED BY 'YourStrongPassword123!';-- 创建业务数据库CREATE DATABASE travel_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;-- 创建业务用户CREATE USER 'travel_user'@'localhost' IDENTIFIED BY 'TravelUserPassword123!';GRANT SELECT, INSERT, UPDATE, DELETE ON travel_db.* TO 'travel_user'@'localhost';FLUSH PRIVILEGES;-- 安全加固DELETE FROM mysql.user WHERE User='';DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');DROP DATABASE IF EXISTS test;FLUSH PRIVILEGES;# 修改 MySQL 配置(my.cnf)vim /etc/my.cnf[mysqld]# 基础配置port = 3306socket = /var/lib/mysql/mysql.sockdatadir = /var/lib/mysql# 字符集character-set-server = utf8mb4collation-server = utf8mb4_unicode_ci# 连接数max_connections = 500max_user_connections = 200# 慢查询slow_query_log = 1slow_query_log_file = /var/lib/mysql/localhost-slow.loglong_query_time = 2# InnoDB 优化innodb_buffer_pool_size = 1G # 服务器内存的 50-70%innodb_log_file_size = 256Minnodb_flush_log_at_trx_commit = 2# 安全bind-address = 127.0.0.1 # 只允许本地访问local-infile = 0[client]default-character-set = utf8mb4systemctl restart mysqld1.5 安装 Redis
# CentOSyum install -y redis# Ubuntuapt install -y redis-server# 修改配置vim /etc/redis.conf# 修改以下项:# bind 127.0.0.1 # 只允许本地访问# requirepass YourRedisPass # 设置密码# port 6379 # 建议改端口# protected-mode yes # 保护模式# maxmemory 256mb # 最大内存# maxmemory-policy allkeys-lru # 淘汰策略# 启动systemctl start redissystemctl enable redis# 验证redis-cli -a YourRedisPass ping# PONG1.6 安装 Nginx
# CentOS(需先安装 EPEL)yum install -y nginx# Ubuntuapt install -y nginx# 启动并设置开机自启systemctl start nginxsystemctl enable nginx# 验证nginx -vcurl -I http://127.0.0.1# HTTP/1.1 200 OK# Nginx 配置文件位置# 主配置:/etc/nginx/nginx.conf# 站点配置:/etc/nginx/conf.d/ (CentOS)# 站点配置:/etc/nginx/sites-available/ (Ubuntu)1.7 防火墙配置
# === CentOS(firewalld)===systemctl start firewalldsystemctl enable firewalld# 放行端口firewall-cmd --permanent --add-port=80/tcpfirewall-cmd --permanent --add-port=443/tcpfirewall-cmd --permanent --add-port=22/tcpfirewall-cmd --reload# 查看已放行端口firewall-cmd --list-ports# === Ubuntu(ufw)===ufw allow 22/tcpufw allow 80/tcpufw allow 443/tcpufw enableufw status原则:3306(MySQL)和 6379(Redis)绝对不要对公网放行,只允许 127.0.0.1 访问。
1.8 云服务器安全组
在华为云 / 阿里云 / 腾讯云控制台的安全组入方向规则中放行:
| 端口 | 用途 | 对外开放 |
|---|---|---|
| 22 | SSH | ✅(建议限制 IP) |
| 80 | HTTP | ✅ |
| 443 | HTTPS | ✅ |
| 3306 | MySQL | ❌ |
| 6379 | Redis | ❌ |
| 8080 | Spring Boot | ❌ |
二、后端部署(Spring Boot)
2.1 本地打包
# 在项目根目录mvn clean package -DskipTests# 产物ls -la target/*.jar# target/my-project.jar2.2 上传到服务器
# 方式 1:scp 上传scp target/my-project.jar root@1.94.33.51:/www/wwwroot/upload/# 方式 2:rsync 上传(断点续传,大文件推荐)rsync -avz --progress target/my-project.jar root@1.94.33.51:/www/wwwroot/upload/# 方式 3:服务器上 rz 接收cd /www/wwwroot/upload/rz # 选择文件# 移动到正式目录mv /www/wwwroot/upload/my-project.jar /www/wwwroot/api/2.3 配置文件外部化
# 创建外部配置文件(优先级高于 jar 内配置)vim /www/wwwroot/api/application.ymlserver: port: 8080spring: datasource: url: jdbc:mysql://127.0.0.1:3306/travel_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8 username: travel_user password: TravelUserPassword123! hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 redis: host: 127.0.0.1 port: 6379 password: YourRedisPass database: 0 lettuce: pool: max-active: 20 max-idle: 10 min-idle: 5mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0logging: file: name: /www/wwwroot/api/logs/application.log level: root: INFO com.example: DEBUG2.4 创建 systemd 服务
cat > /etc/systemd/system/api.service << 'EOF'[Unit]Description=Travel API ServiceAfter=network.target mysqld.service redis.service[Service]Type=simpleUser=rootWorkingDirectory=/www/wwwroot/apiExecStart=/usr/local/jdk17/bin/java \ -Xms1g -Xmx1g \ -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/www/wwwroot/api/logs/ \ -Dfile.encoding=UTF-8 \ -jar /www/wwwroot/api/my-project.jar \ --spring.config.location=/www/wwwroot/api/application.ymlExecStop=/bin/kill -15 $MAINPIDRestart=on-failureRestartSec=10StandardOutput=append:/www/wwwroot/api/logs/systemd.logStandardError=append:/www/wwwroot/api/logs/systemd-error.log[Install]WantedBy=multi-user.targetEOF# 加载并启动systemctl daemon-reloadsystemctl enable api.servicesystemctl start api.service# 验证systemctl status api.servicecurl http://127.0.0.1:8080/actuator/health2.5 后端管理命令速查
systemctl start api.service # 启动systemctl stop api.service # 停止systemctl restart api.service # 重启systemctl status api.service # 状态systemctl enable api.service # 开机自启systemctl disable api.service # 取消自启# 实时日志journalctl -u api.service -f# 应用日志tail -f /www/wwwroot/api/logs/application.log三、前端部署(Vue)
3.1 本地打包
# .env.production 配置 API 地址为相对路径echo 'VITE_API_BASE_URL=/api' > .env.production# 打包npm run build# 产物在 dist/ls -la dist/3.2 上传到服务器
# 打包 dist 为 tar.gz(比 scp 逐个文件快很多)cd disttar -czf /tmp/dist.tar.gz .# 上传scp /tmp/dist.tar.gz root@1.94.33.51:/www/wwwroot/upload/# 服务器上解压ssh root@1.94.33.51rm -rf /www/wwwroot/web/*tar -zxvf /www/wwwroot/upload/dist.tar.gz -C /www/wwwroot/web/rm -f /www/wwwroot/upload/dist.tar.gz3.3 一键上传脚本(本地执行)
#!/bin/bash# 本地执行:打包前端并上传部署SERVER="root@1.94.33.51"WEB_DIR="/www/wwwroot/web"UPLOAD_DIR="/www/wwwroot/upload"BACKUP_DIR="/www/backup/web"DATE=$(date +%Y%m%d_%H%M%S)echo "===== 前端打包并部署 ====="# 1. 打包echo "[1/4] 打包前端..."npm run buildcd disttar -czf /tmp/dist.tar.gz .# 2. 上传echo "[2/4] 上传到服务器..."scp /tmp/dist.tar.gz ${SERVER}:${UPLOAD_DIR}/# 3. 远程部署echo "[3/4] 远程部署..."ssh ${SERVER} << EOFmkdir -p ${BACKUP_DIR}tar -czf ${BACKUP_DIR}/web_${DATE}.tar.gz -C ${WEB_DIR} .rm -rf ${WEB_DIR}/*tar -zxvf ${UPLOAD_DIR}/dist.tar.gz -C ${WEB_DIR}/rm -f ${UPLOAD_DIR}/dist.tar.gznginx -s reloadecho "前端部署完成"# 清理超过 5 个的备份cd ${BACKUP_DIR} && ls -t web_*.tar.gz | tail -n +6 | xargs -r rmEOFecho "[4/4] 完成!"四、Nginx 配置
4.1 创建站点配置文件
# CentOSvim /etc/nginx/conf.d/www.example.com.conf# Ubuntuvim /etc/nginx/sites-available/www.example.com.conf# Ubuntu 还需要创建软链接:# ln -s /etc/nginx/sites-available/www.example.com.conf /etc/nginx/sites-enabled/4.2 完整配置(HTTP 版)
server { listen 80; server_name www.example.com; root /www/wwwroot/web; index index.html; client_max_body_size 50m; # Vue Router history 模式 location / { try_files $uri $uri/ /index.html; } # 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; proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 30d; add_header Cache-Control "public, immutable"; } # index.html 不缓存 location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; } # 禁止访问隐藏文件 location ~ /\. { deny all; } # 日志 access_log /www/wwwlogs/www.example.com.access.log; error_log /www/wwwlogs/www.example.com.error.log;}4.3 proxy_pass 路径规则(高频踩坑)
# 带斜杠:/api/xxx → /xxx(去掉 /api 前缀)location /api/ { proxy_pass http://127.0.0.1:8080/;}# 不带斜杠:/api/xxx → /api/xxx(保留 /api 前缀)location /api/ { proxy_pass http://127.0.0.1:8080;}后端 Controller 不含
/api前缀 → 用带斜杠;含/api前缀 → 用不带斜杠。
4.4 测试并生效
# 测试配置语法nginx -t# 重新加载(不中断服务)nginx -s reload# 验证curl -I http://127.0.0.1curl http://127.0.0.1/api/actuator/health4.5 Nginx 日志切割
# 创建日志切割脚本cat > /www/backup/nginx_log_rotate.sh << 'EOF'#!/bin/bashLOG_DIR="/www/wwwlogs"DATE=$(date +%Y%m%d)# 遍历所有 .log 文件for log_file in ${LOG_DIR}/*.log; do if [ -f "$log_file" ]; then # 重命名 mv "$log_file" "${log_file}.${DATE}" # 压缩 gzip "${log_file}.${DATE}" fidone# 通知 Nginx 重新打开日志文件nginx -s reopen# 删除 30 天前的日志find $LOG_DIR -name "*.log.*.gz" -mtime +30 -deleteEOFchmod +x /www/backup/nginx_log_rotate.sh# 添加到 crontab(每天凌晨 0 点)crontab -e# 添加:0 0 * * * /www/backup/nginx_log_rotate.sh >> /www/wwwlogs/rotate.log 2>&1五、HTTPS 配置
5.1 安装 certbot
# CentOSyum install -y certbot python3-certbot-nginx# Ubuntuapt install -y certbot python3-certbot-nginx5.2 申请 Let's Encrypt 证书
# 方式 1:certbot 自动修改 Nginx 配置(推荐)certbot --nginx -d www.example.com -d example.com# 方式 2:只申请证书,手动配置 Nginxcertbot certonly --nginx -d www.example.com -d example.com# 证书位置:# 证书:/etc/letsencrypt/live/www.example.com/fullchain.pem# 私钥:/etc/letsencrypt/live/www.example.com/privkey.pem5.3 手动配置 Nginx HTTPS
vim /etc/nginx/conf.d/www.example.com.conf# HTTP 跳转 HTTPSserver { listen 80; server_name www.example.com example.com; return 301 https://www.example.com$request_uri;}# HTTPSserver { listen 443 ssl http2; server_name www.example.com; # SSL 证书 ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem; # SSL 优化 ssl_session_timeout 1d; ssl_session_cache shared:MozSSL:10m; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off; # 安全头 add_header Strict-Transport-Security "max-age=63072000" always; add_header X-Frame-Options "SAMEORIGIN"; add_header X-Content-Type-Options "nosniff"; add_header X-XSS-Protection "1; mode=block"; root /www/wwwroot/web; index index.html; client_max_body_size 50m; location / { try_files $uri $uri/ /index.html; } 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 ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 30d; add_header Cache-Control "public, immutable"; } location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; } access_log /www/wwwlogs/www.example.com.access.log; error_log /www/wwwlogs/www.example.com.error.log;}nginx -t && nginx -s reload5.4 证书自动续期
# 测试续期certbot renew --dry-run# 添加定时任务(每月 1 号凌晨 3 点检查续期)crontab -e# 添加:0 3 1 * * /usr/bin/certbot renew --quiet --post-hook "nginx -s reload"六、数据库运维
6.1 自动备份脚本
cat > /www/backup/mysql_backup.sh << 'SCRIPT'#!/bin/bash# MySQL 自动备份脚本BACKUP_DIR="/www/backup/mysql"DB_USER="root"DB_PASS="YourStrongPassword123!"DB_NAME="travel_db"DATE=$(date +%Y%m%d)KEEP_DAYS=7mkdir -p $BACKUP_DIR# 备份并压缩mysqldump -u$DB_USER -p$DB_PASS \ --single-transaction \ --routines \ --triggers \ $DB_NAME | gzip > $BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz# 检查备份是否成功if [ $? -eq 0 ]; then SIZE=$(du -h $BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz | awk '{print $1}') echo "[$(date '+%Y-%m-%d %H:%M:%S')] 备份成功: ${DB_NAME}_${DATE}.sql.gz ($SIZE)"else echo "[$(date '+%Y-%m-%d %H:%M:%S')] ❌ 备份失败!" # 可在此发送告警通知fi# 删除超过 KEEP_DAYS 天的备份find $BACKUP_DIR -name "${DB_NAME}_*.sql.gz" -mtime +$KEEP_DAYS -deleteSCRIPTchmod +x /www/backup/mysql_backup.sh# 添加定时任务(每天凌晨 3 点)crontab -e# 添加:0 3 * * * /www/backup/mysql_backup.sh >> /www/backup/mysql/backup.log 2>&16.2 数据恢复
# 解压并恢复gunzip < /www/backup/mysql/travel_db_20240115.sql.gz | mysql -u root -p travel_db# 或分两步cd /www/backup/mysql/gunzip travel_db_20240115.sql.gzmysql -u root -p travel_db < travel_db_20240115.sql6.3 慢查询排查
# 查看慢查询日志tail -f /var/lib/mysql/localhost-slow.log# 用 mysqldumpslow 分析(按时间排序,前 10 条)mysqldumpslow -s t -t 10 /var/lib/mysql/localhost-slow.log# 按次数排序mysqldumpslow -s c -t 10 /var/lib/mysql/localhost-slow.log6.4 常用排查 SQL
-- 当前连接数SHOW STATUS LIKE 'Threads_connected';-- 查看所有连接SHOW PROCESSLIST;-- 数据库大小SELECT table_schema, ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'MB'FROM information_schema.tables WHERE table_schema = 'travel_db';-- 表大小排序SELECT table_name, ROUND(data_length / 1024 / 1024, 2) AS '数据MB', ROUND(index_length / 1024 / 1024, 2) AS '索引MB', table_rowsFROM information_schema.tablesWHERE table_schema = 'travel_db'ORDER BY data_length DESC;-- 杀掉卡住的连接KILL <ID>;七、日志管理
7.1 Spring Boot 日志配置
vim /www/wwwroot/api/application.ymllogging: file: name: /www/wwwroot/api/logs/application.log level: root: INFO com.example: DEBUG logback: rollingpolicy: max-file-size: 100MB max-history: 30 total-size-cap: 3GB7.2 logback-spring.xml(放在 resources 目录)
<?xml version="1.0" encoding="UTF-8"?><configuration> <property name="LOG_PATH" value="/www/wwwroot/api/logs"/> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/application.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxFileSize>100MB</maxFileSize> <maxHistory>30</maxHistory> <totalSizeCap>3GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/error.log</file> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxFileSize>50MB</maxFileSize> <maxHistory>90</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> <appender-ref ref="ERROR_FILE"/> </root></configuration>7.3 日志查看常用命令
# 实时查看应用日志tail -f /www/wwwroot/api/logs/application.log# 搜索 ERRORgrep "ERROR" /www/wwwroot/api/logs/application.log# 今天的错误数量grep "$(date +%Y-%m-%d)" /www/wwwroot/api/logs/application.log | grep "ERROR" | wc -l# systemd 日志journalctl -u api.service -f # 实时journalctl -u api.service --since today # 今天的journalctl -u api.service --since "1 hour ago" # 最近 1 小时# Nginx 访问日志分析awk '{print $1}' /www/wwwlogs/www.example.com.access.log | sort | uniq -c | sort -rn | head -10 # Top 10 IPawk '{print $9}' /www/wwwlogs/www.example.com.access.log | sort | uniq -c | sort -rn # 状态码分布awk '{print $7}' /www/wwwlogs/www.example.com.access.log | sort | uniq -c | sort -rn | head -20 # Top 20 URL八、监控与告警
8.1 命令行监控
# CPUtop -bn1 | head -5# 或mpstat 1 3# 内存free -h# 磁盘df -hdu -sh /www/wwwroot/*# 磁盘 IOiostat -x 1 3# 网络连接ss -tlnp # 查看监听端口ss -s # 连接数统计# Java 进程jps -l # 查看 Java 进程jstat -gcutil <PID> 1000 # 每 1 秒查看 GC 情况jmap -heap <PID> # 查看堆内存jstack <PID> # 查看线程栈(排查死锁)8.2 资源监控脚本
cat > /www/backup/monitor.sh << 'EOF'#!/bin/bash# 服务器资源监控脚本# CPU 使用率CPU_IDLE=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}' | cut -d'%' -f1)CPU_USAGE=$(echo "100 - $CPU_IDLE" | bc)CPU_INT=${CPU_USAGE%.*}# 内存使用率MEM_TOTAL=$(free | awk '/Mem/ {print $2}')MEM_USED=$(free | awk '/Mem/ {print $3}')MEM_USAGE=$((MEM_USED * 100 / MEM_TOTAL))# 磁盘使用率DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | cut -d'%' -f1)# 磁盘 inode 使用率INODE_USAGE=$(df -i / | awk 'NR==2 {print $5}' | cut -d'%' -f1)# 输出echo "===== $(date '+%Y-%m-%d %H:%M:%S') ====="echo "CPU: ${CPU_USAGE}%"echo "内存: ${MEM_USAGE}%"echo "磁盘: ${DISK_USAGE}%"echo "Inode: ${INODE_USAGE}%"# 告警阈值ALERT_CPU=80ALERT_MEM=80ALERT_DISK=80# 告警(可对接企业微信/钉钉)send_alert() { MSG=$1 echo "[ALERT] $MSG" # curl 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY' \ # -H 'Content-Type: application/json' \ # -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"⚠️ $(hostname) - $MSG\"}}"}if [ $CPU_INT -gt $ALERT_CPU ]; then send_alert "CPU 使用率过高: ${CPU_USAGE}%"fiif [ $MEM_USAGE -gt $ALERT_MEM ]; then send_alert "内存使用率过高: ${MEM_USAGE}%"fiif [ $DISK_USAGE -gt $ALERT_DISK ]; then send_alert "磁盘使用率过高: ${DISK_USAGE}%"fi# 检查关键服务for svc in mysqld redis nginx api; do if ! systemctl is-active $svc > /dev/null 2>&1; then send_alert "服务 $svc 未运行!" fidoneEOFchmod +x /www/backup/monitor.sh# 添加定时任务(每 5 分钟检查)crontab -e# 添加:*/5 * * * * /www/backup/monitor.sh >> /www/wwwlogs/monitor.log 2>&18.3 Spring Boot Actuator
<!-- pom.xml --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency>management: endpoints: web: exposure: include: health,info,metricscurl http://127.0.0.1:8080/actuator/health# {"status":"UP"}curl http://127.0.0.1:8080/actuator/metrics/jvm.memory.used注意:Actuator 端点不要通过 Nginx 暴露到公网,只在服务器内网访问。
8.4 可选:安装轻量监控工具
# htop — 比 top 更友好的进程监控yum install -y htop # CentOSapt install -y htop # Ubuntu# iftop — 网络流量监控yum install -y iftopapt install -y iftop# nethogs — 按进程显示网络流量yum install -y nethogsapt install -y nethogs# 使用htopiftop -nnethogs九、日常运维操作
9.1 后端更新脚本
cat > /www/wwwroot/api/deploy.sh << 'EOF'#!/bin/bash# 后端更新部署脚本APP_NAME="my-project"APP_DIR="/www/wwwroot/api"UPLOAD_DIR="/www/wwwroot/upload"BACKUP_DIR="${APP_DIR}/backup"DATE=$(date +%Y%m%d_%H%M%S)echo "===== 后端部署开始 ====="# 1. 检查新 jar 是否已上传if [ ! -f "${UPLOAD_DIR}/${APP_NAME}.jar" ]; then echo "❌ 未找到 ${UPLOAD_DIR}/${APP_NAME}.jar,请先上传" exit 1fi# 2. 停止服务echo "[1/5] 停止服务..."systemctl stop api.servicesleep 2# 3. 备份旧版本echo "[2/5] 备份旧版本..."mkdir -p $BACKUP_DIRif [ -f "${APP_DIR}/${APP_NAME}.jar" ]; then cp "${APP_DIR}/${APP_NAME}.jar" "${BACKUP_DIR}/${APP_NAME}_${DATE}.jar" echo " 备份: ${BACKUP_DIR}/${APP_NAME}_${DATE}.jar"fi# 4. 替换新版本echo "[3/5] 替换新版本..."cp "${UPLOAD_DIR}/${APP_NAME}.jar" "${APP_DIR}/${APP_NAME}.jar"rm -f "${UPLOAD_DIR}/${APP_NAME}.jar"# 5. 启动服务echo "[4/5] 启动服务..."systemctl start api.service# 6. 健康检查echo "[5/5] 健康检查..."sleep 10for i in 1 2 3 4 5; do if curl -s http://127.0.0.1:8080/actuator/health | grep -q "UP"; then echo "✅ 部署成功!服务已启动" # 清理超过 10 个的旧备份 cd $BACKUP_DIR && ls -t ${APP_NAME}_*.jar | tail -n +11 | xargs -r rm exit 0 fi echo " 等待服务启动... ($i/5)" sleep 5doneecho "❌ 服务启动失败!"echo "--- 最近 30 行日志 ---"tail -30 ${APP_DIR}/logs/application.logexit 1EOFchmod +x /www/wwwroot/api/deploy.sh使用方式:
# 1. 本地上传新 jarscp target/my-project.jar root@1.94.33.51:/www/wwwroot/upload/# 2. 服务器执行部署ssh root@1.94.33.51/www/wwwroot/api/deploy.sh9.2 回滚脚本
cat > /www/wwwroot/api/rollback.sh << 'EOF'#!/bin/bash# 回滚到指定版本APP_NAME="my-project"APP_DIR="/www/wwwroot/api"BACKUP_DIR="${APP_DIR}/backup"# 列出可用备份echo "可用备份版本:"ls -lt $BACKUP_DIR/${APP_NAME}_*.jar 2>/dev/null | awk '{print NR". "$9" ("$6" "$7" "$8")"}'if [ $? -ne 0 ] || [ -z "$(ls $BACKUP_DIR/${APP_NAME}_*.jar 2>/dev/null)" ]; then echo "❌ 没有可用备份" exit 1fi# 选择版本read -p "输入要回滚的序号: " NUMVERSION_FILE=$(ls -t $BACKUP_DIR/${APP_NAME}_*.jar | sed -n "${NUM}p")if [ -z "$VERSION_FILE" ]; then echo "❌ 无效选择" exit 1fiecho "回滚到: $VERSION_FILE"# 停止服务systemctl stop api.service# 回滚cp "$VERSION_FILE" "${APP_DIR}/${APP_NAME}.jar"# 启动服务systemctl start api.servicesleep 10if curl -s http://127.0.0.1:8080/actuator/health | grep -q "UP"; then echo "✅ 回滚成功"else echo "❌ 回滚后启动失败,请检查日志" tail -30 ${APP_DIR}/logs/application.logfiEOFchmod +x /www/wwwroot/api/rollback.sh# 使用# /www/wwwroot/api/rollback.sh9.3 磁盘清理
#!/bin/bash# 磁盘清理脚本echo "===== 磁盘清理 ====="# 1. 系统日志echo "[1] 清理 systemd 日志..."journalctl --vacuum-size=500M# 2. Nginx 旧日志echo "[2] 清理 Nginx 旧日志..."find /www/wwwlogs -name "*.log.*.gz" -mtime +30 -delete# 3. 旧备份echo "[3] 清理旧备份..."find /www/backup -name "*.gz" -mtime +30 -deletefind /www/backup -name "*.tar.gz" -mtime +30 -deletefind /www/wwwroot/api/backup -name "*.jar" -mtime +30 -delete# 4. yum/apt 缓存echo "[4] 清理包管理器缓存..."yum clean all 2>/dev/null || apt clean 2>/dev/null# 5. 临时文件echo "[5] 清理临时文件..."rm -rf /tmp/*.tmp 2>/dev/nullfind /var/tmp -type f -mtime +7 -delete 2>/dev/nullecho "===== 清理完成 ====="df -h /十、常见问题排查
10.1 接口 502 Bad Gateway
# 原因:Nginx 找不到后端服务# 1. 检查后端是否运行systemctl status api.serviceps aux | grep java# 2. 检查端口ss -tlnp | grep 8080# 3. 检查 Nginx 配置nginx -tgrep proxy_pass /etc/nginx/conf.d/*.conf# 4. 查看后端日志tail -50 /www/wwwroot/api/logs/application.logjournalctl -u api.service --since "10 min ago"10.2 接口 504 Gateway Timeout
# 原因:后端处理太慢# 1. 查看后端处理时间grep "request completed" /www/wwwroot/api/logs/application.log | tail -20# 2. 检查慢 SQLtail -f /var/lib/mysql/localhost-slow.log# 3. 增加 Nginx 超时(临时)# 在 location /api/ 中添加:# proxy_read_timeout 120s;# 4. 检查 JVM 内存是否不足jmap -heap $(jps | grep my-project | awk '{print $1}')10.3 前端白屏
# 1. 检查文件是否存在ls -la /www/wwwroot/web/index.htmlls -la /www/wwwroot/web/assets/# 2. 检查 Nginx root 路径grep root /etc/nginx/conf.d/www.example.com.conf# 3. 检查 index.html 中的资源路径cat /www/wwwroot/web/index.html | grep -o 'src="[^"]*"'cat /www/wwwroot/web/index.html | grep -o 'href="[^"]*"'# 4. Nginx error logtail -20 /www/wwwlogs/www.example.com.error.log10.4 Java OOM
# 1. 查看是否被 OOM Killer 杀掉dmesg | grep -i "out of memory"dmesg | grep -i "killed process"# 2. 查看应用日志grep "OutOfMemoryError" /www/wwwroot/api/logs/application.log# 3. 检查堆快照ls -la /www/wwwroot/api/logs/*.hprof# 4. 查看当前内存jmap -heap $(jps | grep my-project | awk '{print $1}')# 解决:增加 -Xmx 或排查内存泄漏10.5 端口被占用
# 查看端口占用ss -tlnp | grep 8080# 或lsof -i:8080# 杀掉占用进程kill -15 <PID>kill -9 <PID> # 强制10.6 MySQL 连接数过多
SHOW STATUS LIKE 'Threads_connected';SHOW VARIABLES LIKE 'max_connections';SHOW PROCESSLIST;-- 杀掉空闲连接KILL <ID>;-- 临时增加连接数SET GLOBAL max_connections = 500;# 永久修改vim /etc/my.cnf# [mysqld]# max_connections = 500systemctl restart mysqld10.7 Redis 连接失败
# 检查 Redis 状态systemctl status redisredis-cli -a YourRedisPass ping# 检查端口ss -tlnp | grep 6379# 查看 Redis 日志tail -f /var/log/redis/redis.log# 常见原因:# 1. 密码不对(application.yml 与 redis.conf 不一致)# 2. Redis 未启动# 3. bind 限制10.8 SSL 证书过期
# 查看证书有效期openssl s_client -connect www.example.com:443 2>/dev/null | openssl x509 -noout -dates# 手动续期certbot renewnginx -s reload十一、crontab 定时任务总览
# 编辑定时任务crontab -e# === 推荐的定时任务 ===# MySQL 每日备份(凌晨 3 点)0 3 * * * /www/backup/mysql_backup.sh >> /www/backup/mysql/backup.log 2>&1# Nginx 日志切割(每天凌晨 0 点)0 0 * * * /www/backup/nginx_log_rotate.sh >> /www/wwwlogs/rotate.log 2>&1# 资源监控(每 5 分钟)*/5 * * * * /www/backup/monitor.sh >> /www/wwwlogs/monitor.log 2>&1# SSL 证书续期检查(每月 1 号凌晨 3 点)0 3 1 * * /usr/bin/certbot renew --quiet --post-hook "nginx -s reload"# 磁盘清理(每周日凌晨 4 点)0 4 * * 0 find /www/backup -name "*.gz" -mtime +30 -delete && journalctl --vacuum-size=500M# 查看所有定时任务crontab -l十二、安全加固
12.1 SSH 加固
vim /etc/ssh/sshd_config# 修改以下配置:Port 22222 # 修改默认端口PermitRootLogin no # 禁止 root 直接登录(用普通用户 + sudo)PasswordAuthentication no # 禁用密码登录(用密钥)MaxAuthTries 3 # 最大认证次数LoginGraceTime 30 # 登录超时systemctl restart sshd# 配置密钥登录(在本地执行)ssh-keygen -t rsa -b 4096ssh-copy-id -p 22222 username@1.94.33.5112.2 Fail2Ban 防暴力破解
# 安装yum install -y fail2ban # CentOSapt install -y fail2ban # Ubuntu# 配置cat > /etc/fail2ban/jail.local << 'EOF'[DEFAULT]bantime = 3600findtime = 600maxretry = 3[sshd]enabled = trueport = 22222maxretry = 3bantime = 3600EOFsystemctl enable fail2bansystemctl start fail2ban# 查看状态fail2ban-client status sshd12.3 MySQL 安全
# 运行安全配置脚本(首次安装后必做)mysql_secure_installation-- 确认 root 只允许本地访问SELECT User, Host FROM mysql.user WHERE User='root';-- 如果有 root@'%',删除:DROP USER 'root'@'%';-- 业务用户只授权需要的权限GRANT SELECT, INSERT, UPDATE, DELETE ON travel_db.* TO 'travel_user'@'localhost';-- 不要用 GRANT ALL12.4 Redis 安全
vim /etc/redis.conf# bind 127.0.0.1 # 只允许本地访问# requirepass YourRedisPass # 必须设置密码# protected-mode yes # 保护模式# port 6379 # 建议改端口# rename-command FLUSHALL "" # 禁用危险命令# rename-command CONFIG "" # 禁用远程配置systemctl restart redis12.5 Nginx 安全头
# 在 server 块中添加:add_header X-Frame-Options "SAMEORIGIN";add_header X-Content-Type-Options "nosniff";add_header X-XSS-Protection "1; mode=block";add_header Referrer-Policy "strict-origin-when-cross-origin";# HTTPS 才加这行:add_header Strict-Transport-Security "max-age=63072000" always;12.6 系统安全检查清单
# 检查异常登录last | head -20# 检查异常进程ps aux | grep -v "USER" | sort -k3 -rn | head -20# 检查异常网络连接ss -tunlp# 检查定时任务(所有用户)for user in $(cut -f1 -d: /etc/passwd); do crontab -u $user -l 2>/dev/null && echo "用户: $user"done# 检查 SUID 文件(可能被提权)find / -perm -4000 -type f 2>/dev/null# 检查最近修改的文件find /www/wwwroot -mtime -1 -type f附录:部署 Checklist
首次部署
□ 域名已解析到服务器 IP□ 系统已更新(yum update / apt upgrade)□ JDK 已安装,java -version 正常□ MySQL 已安装,root 密码已修改,业务库已创建,安全加固已完成□ Redis 已安装,密码已设置,只允许本地访问□ Nginx 已安装□ 防火墙已配置,只开放 22/80/443□ 云安全组已配置□ SSH 已加固(改端口 + 密钥登录)□ 后端 jar 已上传,application.yml 已配置□ systemd 服务已创建并启动□ curl 127.0.0.1:8080/actuator/health 返回 UP□ 前端 dist 已上传到 /www/wwwroot/web/□ Nginx 站点配置已创建,nginx -t 通过□ SSL 证书已申请,HTTPS 已配置□ HTTP 已 301 跳转到 HTTPS□ crontab 定时任务已配置(备份/监控/日志切割/证书续期)□ Fail2Ban 已安装并运行每次更新
□ 后端:上传 jar → 执行 deploy.sh → 健康检查□ 前端:打包 → 上传 → 解压 → nginx -s reload□ 核心功能测试通过□ 旧版本已备份每周检查
□ crontab -l 确认定时任务正常□ 查看监控日志 /www/wwwlogs/monitor.log□ 查看备份日志 /www/backup/mysql/backup.log□ df -h 检查磁盘□ 检查 SSL 证书有效期□ 检查 Nginx 错误日志□ 检查应用错误日志 error.log与宝塔版的对比
| 维度 | 宝塔面板版 | 纯命令行版 |
|---|---|---|
| 上手难度 | ★☆☆☆☆ 可视化操作 | ★★★☆☆ 需要熟悉 Linux |
| 环境安装 | 软件商店一键安装 | yum/apt 手动安装 |
| 网站管理 | 面板创建站点 | 手动写 Nginx conf |
| 文件管理 | 面板文件管理器 | vim/scp/rz |
| SSL 证书 | 面板一键申请 | certbot 命令行 |
| 定时任务 | 面板计划任务 | crontab -e |
| 监控告警 | 面板自带监控 | 自写脚本 |
| 安全加固 | 面板部分支持 | 全手动配置 |
| 可控性 | ★★★☆☆ | ★★★★★ |
| 适合场景 | 新手 / 快速部署 | 专业运维 / 需要完全掌控 |
建议:如果你是独立开发者 + 运维,命令行版虽然前期学习成本高,但后期效率更高、可控性更强、不依赖第三方面板。如果团队里有不熟悉 Linux 的人,宝塔版更友好。
总结
纯命令行部署的核心就三句话:
- 环境标准化——所有软件用包管理器安装,配置文件位置统一,版本可追溯
- 服务 systemd 化——后端用 systemd 管理,开机自启、自动重启、日志统一
- 运维脚本化——部署、备份、监控、回滚全部脚本化,减少手动操作出错
命令行操作的好处是你完全知道每一步在做什么,出问题能快速定位。 宝塔面板帮你做了很多封装,但封装同时也是黑盒——出了问题你不知道是哪个环节的配置不对。 两个版本没有优劣,选择适合你当前阶段的就好。