结论: 服务器部署项目的标准流程是:上传构建产物到 /data/app/ → 安装运行依赖 → 用 systemd 或 PM2 托管进程并设置开机自启 → 配置 Nginx 反向代理到应用端口 → 绑定域名并配 SSL。小项目可用 scp/rsync 直接上传,团队协作推荐 Git 拉取或 CI/CD,多环境隔离项目优先 Docker Compose。
"把项目部署到服务器"这件事,看似就是把代码传上去跑起来,但实际会牵扯一长串决策:用 root 还是普通用户?服务挂了谁来拉起?发布时用户会断连吗?回滚怎么做?这些问题不提前想清楚,上线后的每一次更新都是一次冒险。
本文给出四种由简到繁的部署路径,并重点讲生产环境必备的三样东西:进程托管、反向代理、零停机发布。命令以 Ubuntu 24.04 LTS 为主,同时给出 Rocky Linux 9 对照。截至 2026 年,主流运行时版本为 JDK 21 LTS、Node.js 22 LTS、Python 3.12、Docker 27.x。
部署前:先确定这四个问题
结论:动手前明确部署路径、运行用户、目录规范和端口分配,能避免 80% 的后期返工。 这四项是所有部署方案的公共基础,无论你后面选哪条路径都适用。
| 决策项 | 推荐做法 | 原因 |
|---|---|---|
| 运行用户 | 专用用户 appuser,禁止 root | 被攻破时限制横向影响 |
| 代码目录 | /data/app/ 或 /opt/appname/ | 与系统目录隔离,便于备份 |
| 版本目录 | /data/app/releases/v1.2.3 + 软链 current | 秒级回滚的基础 |
| 日志目录 | /data/logs/appname/ | 便于 logrotate 统一管理 |
| 端口规划 | 应用监听 127.0.0.1:8080,Nginx 对外 80 | 内部端口不直接暴露 |
# 创建专用用户与标准目录结构
sudo useradd -r -m -s /bin/bash appuser
sudo mkdir -p /data/app/releases /data/logs/app /data/backup
sudo chown -R appuser:appuser /data/app /data/logs
# 查看当前用户 uid 确认创建成功
id appuser部署方式怎么演进:手工上传 → 命令行 → CI/CD
结论:部署方式的演进方向,是把"靠人记住的操作步骤"固化成"机器可重复执行的脚本",团队越大、发布越频繁,越应该往右走。 判断标准很简单:参与发布的人数 × 每月发布次数 ≥ 10,就该把手工步骤脚本化;服务器超过 3 台或每天都要发布,就该上 CI/CD。
三种阶段的差异主要体现在可追溯性、回滚速度和人为失误率上:
| 阶段 | 典型做法 | 适用团队规模 | 单次发布耗时 | 主要风险 |
|---|---|---|---|---|
| 手工上传 | FTP 客户端/面板拖拽、scp 传包、手敲命令 | 1~2 人,每月 1~2 次 | 5~15 分钟 | 无版本记录,回滚靠人肉记命令 |
| 命令行部署 | rsync + deploy.sh + systemd 托管 | 2~10 人,每周 1~2 次 | 1~3 分钟 | 脚本自身没进版本库会逐渐漂移 |
| CI/CD 自动部署 | GitHub Actions / GitLab CI + 镜像仓库 + SSH 触发 | 10 人以上,每天多次 | 30 秒~2 分钟 | 流水线权限与密钥管理不当会变成新攻击面 |
需要注意的是,演进不是"越高级越好"。个人博客、内部小工具用手工上传完全够用,硬上 CI/CD 反而多了一套需要维护的系统。真正必须脚本化的场景只有两类:多台服务器要保持一致性,以及发布回滚必须可重复执行。除此之外,选你能看懂、能排错的那一档即可。
部署前的本地检查清单
结论:发布前花 5 分钟做一次自检,能挡掉绝大多数上线事故,重点是依赖版本、配置差异、数据库迁移、构建产物、密钥五项是否对齐。 这五项是线上故障最高发的来源,尤其是"本地能跑、线上起不来"几乎都出在依赖版本和配置文件上。
- 依赖版本核对:本地
node -v、java -version、python3 -V必须与服务器一致。本地 Node.js 22、服务器 Node.js 18 是典型翻车场景,语法和依赖都可能不兼容。 - 配置文件 diff:把本次要发布的配置与线上正在运行的上一版本配置逐行比对,重点看数据库地址、Redis 地址、域名回调、超时时间。
- 数据库迁移脚本:先备份再执行,且必须保证新旧版本代码能同时兼容这份数据结构(先加字段、后删字段,分两次发布)。
- 静态资源构建:确认
dist/是本次新构建的产物,而不是上一次的残留;带 hash 的文件名能天然规避浏览器缓存问题。 .env与密钥管理:密钥绝不进 Git、绝不打进构建产物;生产环境用 systemd 的EnvironmentFile=或 CI 平台的 Secrets 注入。
# 1) 依赖版本核对:本地与服务器输出必须一致
node -v && npm -v && java -version 2>&1 | head -1
ssh appuser@203.0.113.10 'node -v; npm -v; java -version 2>&1 | head -1'
# 2) 配置文件差异比对(与线上上一版本 release 目录对比)
diff -u /data/app/releases/v1.2.3/application.yml ./src/main/resources/application-prod.yml
# 3) 数据库变更:先备份,再执行迁移脚本
mysqldump -h 127.0.0.1 -u root -p --single-transaction --routines appdb \
> /data/backup/appdb_$(date +%F).sql
mysql -h 127.0.0.1 -u root -p appdb < ./db/migration/V1.2.3__add_index.sql
# 4) 构建产物自检:确认时间戳是本次构建的
ls -lh dist/ && find dist -name '*.js' -newer package.json | head -5
# 5) 敏感信息扫描:确认私钥、AK 没有被打进产物
grep -rlE 'BEGIN (RSA|OPENSSH|PRIVATE) KEY|AKIA[0-9A-Z]{16}' dist/ || echo "no secret found"上面几条命令的参数含义:--single-transaction 让 mysqldump 在一个事务内导出,避免锁表影响线上写入;diff -u 输出 unified 格式便于定位具体行;find -newer 用于确认文件确实是在 package.json 之后生成的;grep -rlE 的 -l 只列出命中文件名,命中即说明密钥泄漏,必须重新构建。
路径一:手动上传部署(最快上手)
结论:适用于个人项目或首次上线,核心是用 scp 或 rsync 把本地构建产物传到服务器,再用 systemd 或 PM2 启动。 rsync 比 scp 好在支持增量同步,第二次发布只传差异部分。
# 本地构建前端项目示例(Vite / Vue / React)
npm ci
npm run build
# 构建产物位于 ./dist/
# 上传 dist 到服务器 Nginx 根目录(--delete 保持与目标一致)
rsync -avz --delete -e "ssh -p 22" ./dist/ appuser@203.0.113.10:/data/app/current/
# 上传后端 jar 包
scp -P 22 ./target/app.jar appuser@203.0.113.10:/data/app/releases/app-v1.2.3.jarrsync 的四个常用参数值得记牢:-a 是归档模式,一次性保留权限、时间戳、符号链接;-v 输出传输明细;-z 在传输时压缩,跨地域上传能省 30%~70% 流量;--delete 会删除目标端多出来的旧文件,保证两边完全一致。--delete 是危险参数,正式执行前务必先跑一次演练:rsync -avz --delete --dry-run ./dist/ appuser@203.0.113.10:/data/app/current/,确认输出里没有误删项再去掉 --dry-run。
后端 Jar 包用 systemd 托管:
[Unit]
Description=App Backend Service
After=network-online.target mysql.service
Wants=network-online.target
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/data/app
Environment="JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseG1GC"
Environment="SPRING_PROFILES_ACTIVE=prod"
Environment="TZ=Asia/Shanghai"
ExecStart=/usr/bin/java $JAVA_OPTS -jar /data/app/current/app.jar
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=10
KillSignal=SIGTERM
TimeoutStopSec=30
StandardOutput=append:/data/logs/app/stdout.log
StandardError=append:/data/logs/app/stderr.log
[Install]
WantedBy=multi-user.target这段 unit 文件里几个关键参数的取舍:Type=simple 表示 ExecStart 启动的进程就是主进程,适合 java -jar 这类前台运行的应用;Restart=always 配合 RestartSec=10 实现崩溃后 10 秒自动拉起;KillSignal=SIGTERM + TimeoutStopSec=30 给应用留出 30 秒处理完存量请求再退出,超时才会被 SIGKILL 强杀;StandardOutput=append: 用追加模式写日志,避免服务重启时覆盖历史日志。
sudo tee /etc/systemd/system/myapp.service > /dev/null < myapp.service
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp --no-pager
# 后续发布的原子化操作
# 1. 停旧 -> 换软链 -> 启新
sudo systemctl stop myapp
sudo rm -f /data/app/current && sudo ln -s /data/app/releases/v1.2.4 /data/app/current
sudo systemctl start myapp
# 回滚:只需把软链指回上一个版本再重启
sudo rm -f /data/app/current && sudo ln -s /data/app/releases/v1.2.3 /data/app/current
sudo systemctl restart myappNode.js 项目用 PM2 管理:
# 安装 PM2 并启动应用
npm install -g pm2
cd /data/app/current
pm2 start ecosystem.config.js --env production
# ecosystem.config.js 示例
module.exports = {
apps: [{
name: 'api-server',
script: './server.js',
instances: 2, # 启动 2 个进程,充分利用多核
exec_mode: 'cluster',
max_memory_restart: '512M', # 内存超 512MB 自动重启,防内存泄漏
env_production: {
NODE_ENV: 'production',
PORT: 8080
}
}]
};
# 保存进程列表并设置开机自启
pm2 save
pm2 startup systemd
# 常用运维命令
pm2 list
pm2 logs api-server --lines 100
pm2 reload api-server # 零停机重载
pm2 monit路径二:Git 拉取部署(团队协作首选)
结论:通过 Git 拉取代码配合构建脚本部署,便于版本追踪与多人协作。 服务器上配置 deploy key 拉取仓库,用一个 deploy.sh 把拉取、依赖安装、构建、重启串成一条命令。
# 服务器上生成部署专用密钥,公钥添加到 Git 仓库的 Deploy Keys
sudo -u appuser ssh-keygen -t ed25519 -C "deploy@web01"
# 首次克隆代码
sudo -u appuser git clone -b main git@github.com:yourorg/yourapp.git /data/app/repo
# 编写部署脚本
cat > /data/app/deploy.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
APP_DIR=/data/app/repo
RELEASE_DIR=/data/app/releases/$(date +%Y%m%d%H%M%S)
cd "$APP_DIR"
git fetch --all --tags
git checkout "${1:-main}"
git pull --ff-only
# 安装依赖并构建
npm ci --omit=dev
npm run build
# 生成新版本目录并复制产物
mkdir -p "$RELEASE_DIR"
cp -r dist/* "$RELEASE_DIR"/
# 原子切换软链
ln -sfn "$RELEASE_DIR" /data/app/current
# 保留最近 5 个版本,其余清理
ls -1dt /data/app/releases/* | tail -n +6 | xargs -r rm -rf
echo "Deploy finished: $RELEASE_DIR"
EOF
chmod +x /data/app/deploy.sh
sudo chown appuser:appuser /data/app/deploy.sh
# 执行部署(可加参数指定分支或 tag)
sudo -u appuser /data/app/deploy.sh v1.2.3脚本头部的 set -euo pipefail 是部署脚本的保命三件套:-e 让任意一条命令失败就立即退出,避免"构建失败却继续发布";-u 遇到未定义变量直接报错;-o pipefail 让管道中任意一环失败也算失败。git pull --ff-only 则保证服务器分支不会被意外 merge 出一条分叉历史,始终与远端保持线性一致。
软链切换 + 保留多版本目录是这套方案的核心价值:发布是原子操作,回滚只需 ln -sfn 指回上一个目录,耗时不到 1 秒。
路径三:Docker Compose 部署(推荐生产)
结论:多服务、多环境、要求可复现的项目用 Docker Compose 最优。 一个 docker-compose.yml 描述全部服务依赖,开发、测试、生产用同一份配置,彻底消除"我这里能跑"问题。
# 安装 Docker(Ubuntu 24.04)
sudo apt install -y docker.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker appuser # 免 sudo 执行 docker,需重新登录生效
# 验证
docker version
docker compose version一个典型的 Compose 文件:
services:
app:
image: registry.example.com/myapp:1.2.3
container_name: myapp
restart: always
depends_on:
redis:
condition: service_healthy
environment:
TZ: Asia/Shanghai
SPRING_PROFILES_ACTIVE: prod
REDIS_HOST: redis
volumes:
- /data/logs/app:/app/logs
- ./application-prod.yml:/app/config/application.yml:ro
ports:
- "127.0.0.1:8080:8080" # 只允许本机访问,外网一律走 Nginx
healthcheck:
test: ["CMD-SHELL", "curl -fs http://localhost:8080/actuator/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 40s
deploy:
resources:
limits:
memory: 1G
cpus: "1.0"
redis:
image: redis:7.4-alpine
container_name: myapp-redis
restart: always
command: redis-server --requirepass Str0ngRedisPass --maxmemory 256mb
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "Str0ngRedisPass", "ping"]
interval: 30s
timeout: 3s
retries: 3
volumes:
redis-data:其中三段配置最容易被忽略却最有用:depends_on 的 condition: service_healthy 保证 Redis 真正 ping 通了才启动应用,而不是"容器起来了就算就绪";healthcheck 的 start_period: 40s 给了 JVM 启动的宽限期,避免启动阶段就被判定为不健康反复重启;deploy.resources.limits 限制单个容器最多用 1 GB 内存、1 个 CPU 核,防止一个服务把整台机器拖垮。
# 启动全部服务(-d 后台运行)
docker compose up -d
# 查看服务状态与健康检查
docker compose ps
docker compose logs -f app --tail 100
# 更新发布:拉取新镜像后重建容器
docker compose pull app
docker compose up -d app
# 回滚:把 image tag 改回上一版本再 up -d
docker compose up -d app路径四:CI/CD 自动发布
结论:团队协作且发布频繁(每周超过 2 次)就该上 CI/CD。 用 GitHub Actions 或 GitLab CI 在代码合并后自动构建镜像、推送仓库,然后通过 SSH 远程执行部署脚本完成发布。
一个 GitHub Actions 部署片段:
name: deploy
on:
push:
tags: ['v*']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Build
run: mvn -B clean package -DskipTests
- name: Copy artifact to server
env:
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
rsync -avz target/*.jar appuser@${{ secrets.SERVER_IP }}:/data/app/releases/
- name: Restart service
run: |
ssh appuser@${{ secrets.SERVER_IP }} 'sudo systemctl restart myapp'流水线的两个安全细节:on.push.tags 意味着只有打 tag 才触发生产发布,日常 push 不会误触;私钥一律走仓库 Secrets,且该私钥对应的服务器账号要用 sudo 白名单限制只能执行 systemctl restart myapp 这一条命令,避免流水线凭据泄漏后拿到整机权限。
企业QQ咨询




