全栈手账 订阅 RSS

Java + Vue 项目部署运维 — 纯命令行版

不使用宝塔面板,纯 Linux 命令行完成 Spring Boot + Vue 3 + MySQL + Redis + Nginx 的部署与运维。适合对服务器有完全掌控力、希望理解每一步操作的场景。

读了 63 分钟 更新于 8 月 5 日

一、服务器环境准备

1.1 系统初始化

bash
# 更新系统(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 dnsutils

1.2 创建项目目录结构

bash
# 统一目录规划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/wwwroot

1.3 安装 JDK 17

bash
# 下载并解压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

bash
# === 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
sql
-- 修改 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;
bash
# 修改 MySQL 配置(my.cnf)vim /etc/my.cnf
ini
[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 = utf8mb4
bash
systemctl restart mysqld

1.5 安装 Redis

bash
# 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# PONG

1.6 安装 Nginx

bash
# 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 防火墙配置

bash
# === 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 本地打包

bash
# 在项目根目录mvn clean package -DskipTests# 产物ls -la target/*.jar# target/my-project.jar

2.2 上传到服务器

bash
# 方式 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 配置文件外部化

bash
# 创建外部配置文件(优先级高于 jar 内配置)vim /www/wwwroot/api/application.yml
yaml
server:  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: DEBUG

2.4 创建 systemd 服务

bash
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/health

2.5 后端管理命令速查

bash
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 本地打包

bash
# .env.production 配置 API 地址为相对路径echo 'VITE_API_BASE_URL=/api' > .env.production# 打包npm run build# 产物在 dist/ls -la dist/

3.2 上传到服务器

bash
# 打包 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.gz

3.3 一键上传脚本(本地执行)

bash
#!/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 创建站点配置文件

bash
# 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 版)

nginx
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 路径规则(高频踩坑)

nginx
# 带斜杠:/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 测试并生效

bash
# 测试配置语法nginx -t# 重新加载(不中断服务)nginx -s reload# 验证curl -I http://127.0.0.1curl http://127.0.0.1/api/actuator/health

4.5 Nginx 日志切割

bash
# 创建日志切割脚本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

bash
# CentOSyum install -y certbot python3-certbot-nginx# Ubuntuapt install -y certbot python3-certbot-nginx

5.2 申请 Let's Encrypt 证书

bash
# 方式 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.pem

5.3 手动配置 Nginx HTTPS

bash
vim /etc/nginx/conf.d/www.example.com.conf
nginx
# 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;}
bash
nginx -t && nginx -s reload

5.4 证书自动续期

bash
# 测试续期certbot renew --dry-run# 添加定时任务(每月 1 号凌晨 3 点检查续期)crontab -e# 添加:0 3 1 * * /usr/bin/certbot renew --quiet --post-hook "nginx -s reload"

六、数据库运维

6.1 自动备份脚本

bash
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>&1

6.2 数据恢复

bash
# 解压并恢复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.sql

6.3 慢查询排查

bash
# 查看慢查询日志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.log

6.4 常用排查 SQL

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 日志配置

bash
vim /www/wwwroot/api/application.yml
yaml
logging:  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: 3GB

7.2 logback-spring.xml(放在 resources 目录)

xml
<?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 日志查看常用命令

bash
# 实时查看应用日志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 命令行监控

bash
# 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 资源监控脚本

bash
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>&1

8.3 Spring Boot Actuator

xml
<!-- pom.xml --><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-actuator</artifactId></dependency>
yaml
management:  endpoints:    web:      exposure:        include: health,info,metrics
bash
curl 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 可选:安装轻量监控工具

bash
# 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 后端更新脚本

bash
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

使用方式:

bash
# 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.sh

9.2 回滚脚本

bash
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.sh

9.3 磁盘清理

bash
#!/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

bash
# 原因: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

bash
# 原因:后端处理太慢# 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 前端白屏

bash
# 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.log

10.4 Java OOM

bash
# 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 端口被占用

bash
# 查看端口占用ss -tlnp | grep 8080# 或lsof -i:8080# 杀掉占用进程kill -15 <PID>kill -9 <PID>   # 强制

10.6 MySQL 连接数过多

sql
SHOW STATUS LIKE 'Threads_connected';SHOW VARIABLES LIKE 'max_connections';SHOW PROCESSLIST;-- 杀掉空闲连接KILL <ID>;-- 临时增加连接数SET GLOBAL max_connections = 500;
bash
# 永久修改vim /etc/my.cnf# [mysqld]# max_connections = 500systemctl restart mysqld

10.7 Redis 连接失败

bash
# 检查 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 证书过期

bash
# 查看证书有效期openssl s_client -connect www.example.com:443 2>/dev/null | openssl x509 -noout -dates# 手动续期certbot renewnginx -s reload

十一、crontab 定时任务总览

bash
# 编辑定时任务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 加固

bash
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.51

12.2 Fail2Ban 防暴力破解

bash
# 安装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 sshd

12.3 MySQL 安全

bash
# 运行安全配置脚本(首次安装后必做)mysql_secure_installation
sql
-- 确认 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 ALL

12.4 Redis 安全

bash
vim /etc/redis.conf# bind 127.0.0.1              # 只允许本地访问# requirepass YourRedisPass   # 必须设置密码# protected-mode yes          # 保护模式# port 6379                   # 建议改端口# rename-command FLUSHALL ""  # 禁用危险命令# rename-command CONFIG ""    # 禁用远程配置systemctl restart redis

12.5 Nginx 安全头

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 系统安全检查清单

bash
# 检查异常登录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 的人,宝塔版更友好。


总结

纯命令行部署的核心就三句话:

  1. 环境标准化——所有软件用包管理器安装,配置文件位置统一,版本可追溯
  2. 服务 systemd 化——后端用 systemd 管理,开机自启、自动重启、日志统一
  3. 运维脚本化——部署、备份、监控、回滚全部脚本化,减少手动操作出错

命令行操作的好处是你完全知道每一步在做什么,出问题能快速定位。 宝塔面板帮你做了很多封装,但封装同时也是黑盒——出了问题你不知道是哪个环节的配置不对。 两个版本没有优劣,选择适合你当前阶段的就好。

前端、后端、部署、客服都是我一个人。写这里的东西都是真踩过的坑,不是抄来的教程。