本文用于给单机或少量服务器上的中小型项目建立一套可复制的生产部署方式。核心思路是:

  • 后端常驻进程交给 systemd 管理。
  • 入口流量、静态资源和反向代理交给 Nginx 管理。
  • 真实环境变量放在服务器 /etc/<app>/<app>.env
  • 项目仓库只保存模板、脚本和说明,不提交真实密钥。

这套方式不追求一开始就容器化、网关化或平台化。先把目录、配置、脚本、验收和回滚边界固定下来,后续项目就能按同一套流程复制。

维护: 本文最后整理日期为 2026-06-09。systemd 和 Nginx 的具体命令可能受发行版影响。落地前应结合当前服务器执行 systemctl --versionnginx -vnginx -T 核对实际环境。

基础初始化

获取服务器

服务器可以直接从阿里云、腾讯云、华为云等云厂商购买。个人博客、小工具或低流量服务,入门阶段通常可以先从 2c2g 规格开始。

拿到服务器后先确认三件事:

  • 公网 IP 或绑定域名。
  • SSH 登录用户和初始密码。
  • 安全组已放行 SSH 端口,默认是 22

不同云厂商的默认登录用户不完全一样,有些是 root,有些是 ubuntuadmin 或控制台创建的用户。第一次使用前建议在控制台重置一次登录密码。

创建工作用户

可以先用初始用户登录服务器:

1
ssh root@<server-ip>

首次连接会提示是否信任主机指纹,确认 IP 无误后输入 yes,SSH 会把服务器指纹写入本机 ~/.ssh/known_hosts

日常不建议长期直接使用 root。可以创建一个普通工作用户,并授予 sudo 权限:

1
2
adduser acs
usermod -aG sudo acs

之后的部署、调试和日常维护都尽量使用普通用户登录,需要管理员权限时再通过 sudo 执行。

配置 SSH 免密登录

本机可以用 ~/.ssh/config 给服务器配置别名:

1
2
3
Host myserver
HostName <server-ip>
User acs

之后就可以直接登录:

1
ssh myserver

生成本机 SSH 密钥:

1
ssh-keygen -t ed25519 -C "<your-email>"

把公钥添加到服务器:

1
ssh-copy-id myserver

如果不能使用 ssh-copy-id,也可以手动把本机 ~/.ssh/id_ed25519.pub 内容追加到服务器用户的 ~/.ssh/authorized_keys

配置常用工具

可以把常用 shell、vim、tmux 配置复制到服务器:

1
scp ~/.bashrc ~/.vimrc ~/.tmux.conf myserver:

Debian / Ubuntu 系统常用初始化命令:

1
2
3
sudo apt update
sudo apt upgrade
sudo apt install git curl vim tmux

适用场景

适合:

  • 单机或少量云服务器部署。
  • 一个项目包含后端服务和前端静态资源。
  • 后端以 jar、二进制或脚本形式常驻运行。
  • Nginx 负责域名、HTTPS、静态资源和 /api/ 反向代理。
  • 团队还没有完整 CI/CD 或容器平台,但需要稳定、可复用的上线流程。

不适合直接照搬:

  • 多副本自动伸缩。
  • Kubernetes / Docker Compose 已经是主部署方式。
  • 需要灰度发布、自动回滚、服务发现和复杂流量治理。
  • 多团队共享同一套 API Gateway。

项目内标准目录

新项目建议保留这些文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
deploy/
<app>.env.example
<app>.service.example
nginx.<app>.conf.example

scripts/
install-runtime.sh
deploy-prod.sh
check-deploy.sh
pull-and-deploy.sh

docs/
deployment.md

.gitattributes

各目录职责:

路径 职责
deploy/ 保存部署模板,不保存真实密钥
scripts/ 保存初始化、发布、验收脚本
docs/deployment.md 保存当前项目实际部署说明
.gitattributes 固定脚本和模板行尾

服务器目录约定

推荐把运行目录固定下来:

1
2
3
4
5
6
/opt/<app>/server.jar
/var/www/<app>
/var/lib/<app>/uploads
/etc/<app>/<app>.env
/etc/systemd/system/<app>.service
/etc/nginx/conf.d/<app>.conf

含义:

路径 用途
/opt/<app>/ 后端 artifact 和运行工作目录
/var/www/<app>/ 前端静态资源
/var/lib/<app>/uploads 上传文件、业务运行数据
/etc/<app>/<app>.env 真实环境变量
/etc/systemd/system/<app>.service systemd 服务文件
/etc/nginx/conf.d/<app>.conf Nginx 项目入口配置

如果服务器使用 Debian / Ubuntu 的 sites-available 习惯,也可以改用:

1
2
/etc/nginx/sites-available/<app>.conf
/etc/nginx/sites-enabled/<app>.conf

Nginx 两种放置方式只选一种。不要同一个项目同时安装到 conf.dsites-enabled,否则可能重复加载同一个 server

固定变量

每个项目开始时先确定这些变量:

1
2
3
4
5
6
7
8
9
10
11
APP_NAME=<app>
APP_USER=<app>
APP_GROUP=<app>
APP_DIR=/opt/<app>
WEB_DIR=/var/www/<app>
STORAGE_DIR=/var/lib/<app>/uploads
ENV_DIR=/etc/<app>
ENV_FILE=/etc/<app>/<app>.env
SERVICE_NAME=<app>
APP_URL=https://example.com
SERVER_PORT=8080

脚本可以允许通过环境变量覆盖默认值,但模板和项目文档里必须写清楚默认值。否则新项目复制时会出现目录、用户、端口不一致的问题。

.gitattributes

新项目根目录应放 .gitattributes,固定跨平台行尾,避免 shell 脚本在 Linux 服务器上因为 CRLF 报错。

推荐模板:

1
2
3
4
*.sh text eol=lf
deploy/*.example text eol=lf
docs/*.md text eol=lf
README.md text eol=lf

如果项目里有 Maven Wrapper、Gradle Wrapper 或其他 Unix 可执行脚本,也要显式固定 LF:

1
2
3
4
mvnw text eol=lf
gradlew text eol=lf
*.cmd text eol=crlf
*.bat text eol=crlf

规则:

  • .gitattributes 应在项目早期创建,避免后续出现大量无意义行尾 diff。
  • 所有 scripts/*.sh 必须是 LF 行尾。
  • 所有部署模板建议是 LF 行尾,复制到 Linux 系统目录后不需要再转换。
  • Windows 专用脚本可以保留 CRLF,例如 *.cmd*.bat
  • 如果新增脚本目录,例如 bin/ops/,要同步补充对应规则。

deploy 模板

环境变量模板

deploy/<app>.env.example 只放配置模板:

  • 数据库连接。
  • 应用端口。
  • 公开访问地址。
  • 上传目录。
  • JWT、管理员初始密码等敏感项的占位符。
  • 不放真实密码、真实 token、真实证书内容。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
SERVER_PORT=8080
APP_PUBLIC_BASE_URL=https://example.com
UPLOAD_DIR=/var/lib/<app>/uploads

DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=<app>
DB_USERNAME=<change-me>
DB_PASSWORD=<change-me>

JWT_SECRET=<change-me>
ADMIN_INITIAL_PASSWORD=<change-me>

systemd 模板

deploy/<app>.service.example 只描述服务怎么启动。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Unit]
Description=<app> service
After=network-online.target
Wants=network-online.target

[Service]
User=<app>
Group=<app>
WorkingDirectory=/opt/<app>
EnvironmentFile=/etc/<app>/<app>.env
ExecStart=/usr/bin/java -jar /opt/<app>/server.jar
Restart=always
RestartSec=5
SuccessExitStatus=143

[Install]
WantedBy=multi-user.target

注意:

  • 不要写死个人用户名。
  • 服务用户要和 install-runtime.sh 创建、授权的用户一致。
  • EnvironmentFile 指向服务器真实 env 文件,不指向仓库里的 .env.example
  • Java、Node、Go 等不同后端只替换 ExecStart,目录和 env 模型尽量保持一致。

Nginx 模板

deploy/nginx.<app>.conf.example 只描述本项目入口。

常见前后端分离项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
listen 80;
server_name example.com;

root /var/www/<app>;
index index.html;

location / {
try_files $uri $uri/ /index.html;
}

location /api/ {
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_pass http://127.0.0.1:8080;
}
}

要求:

  • 一个项目一个 server { ... }
  • server_name 上线时必须改成真实域名。
  • 静态资源指向 WEB_DIR
  • /api/ 反向代理到 127.0.0.1:${SERVER_PORT}
  • SPA 项目使用 try_files $uri $uri/ /index.html
  • 生产环境默认不要公开 Swagger / OpenAPI。

确实需要保留 Swagger / OpenAPI 时,用内网、IP allowlist 或 Basic Auth 限制。

scripts 职责

install-runtime.sh

负责首次初始化服务器运行环境:

  • 创建服务用户和用户组。
  • 创建 APP_DIRWEB_DIRSTORAGE_DIRENV_DIR
  • deploy/<app>.env.example 初始化 ENV_FILE
  • 设置目录 owner 和权限。
  • 检查 systemd service 是否安装。
  • 检查 Nginx config 是否安装且只安装一份。
  • 不构建、不发布、不拉代码。

这个脚本只做“运行时环境初始化”,不要混入发布逻辑。

deploy-prod.sh

负责发布当前工作区代码:

  • 检查 Java、Node、npm、Nginx、systemctl 等依赖。
  • 构建后端。
  • 构建前端。
  • 安装后端 artifact 到 APP_DIR
  • 安装前端 dist 到 WEB_DIR
  • systemctl restart <app>
  • nginx -t
  • systemctl reload nginx
  • 调用 scripts/check-deploy.sh

这个脚本默认发布当前工作区,不自动切分支。正式发布前先人工确认分支和工作区状态。

check-deploy.sh

负责部署验收:

  • 检查 systemd 服务是否 active。
  • 检查首页 HTTP 状态。
  • 检查健康接口或关键 API。
  • 可选检查登录、数据库、安全入口等业务 smoke test。
  • 失败时非 0 退出。

验收项要按项目可配置,不要让所有项目都强依赖 MySQL、登录账号或固定业务接口。

pull-and-deploy.sh

只做便捷封装:

1
2
git pull --ff-only
bash scripts/deploy-prod.sh

它适合低风险小项目的日常发布。正式发布仍建议先人工确认分支、commit、工作区状态,再执行 deploy-prod.sh

首次部署流程

  1. 在服务器准备代码。
1
2
git clone <repo-url> /srv/src/<app>
cd /srv/src/<app>
  1. 初始化运行目录和 env 文件。
1
bash scripts/install-runtime.sh
  1. 安装 systemd 模板。
1
2
3
sudo cp deploy/<app>.service.example /etc/systemd/system/<app>.service
sudo systemctl daemon-reload
sudo systemctl enable <app>
  1. 安装 Nginx 模板,二选一。

conf.d 方式:

1
2
3
sudo cp deploy/nginx.<app>.conf.example /etc/nginx/conf.d/<app>.conf
sudo nginx -t
sudo systemctl reload nginx

sites-enabled 方式:

1
2
3
4
sudo cp deploy/nginx.<app>.conf.example /etc/nginx/sites-available/<app>.conf
sudo ln -sf /etc/nginx/sites-available/<app>.conf /etc/nginx/sites-enabled/<app>.conf
sudo nginx -t
sudo systemctl reload nginx
  1. 编辑真实环境变量。
1
sudo vi /etc/<app>/<app>.env

至少确认:

  • 数据库连接。
  • APP_PUBLIC_BASE_URL
  • 管理员初始密码。
  • JWT secret。
  • 上传目录。
  • 端口是否和 Nginx 代理一致。
  1. 首次发布。
1
bash scripts/deploy-prod.sh
  1. 单独验收。
1
bash scripts/check-deploy.sh

日常发布流程

推荐流程:

1
2
3
4
5
cd /srv/src/<app>
git status --short
git checkout main
git pull --ff-only
bash scripts/deploy-prod.sh

如果项目长期分支是 master,就把 main 改成 master,不要让脚本和文档混用。

快速流程:

1
bash scripts/pull-and-deploy.sh main

如果 pull-and-deploy.sh 支持分支参数,内部仍然要使用 git pull --ff-only,避免服务器上出现隐式 merge commit。

发布前检查

发布前至少检查:

  • 当前分支是否正确。
  • 工作区是否干净。
  • 要发布的 commit 是否符合预期。
  • deploy/*.example 没有真实密钥。
  • scripts/*.sh 是 LF 行尾。
  • Nginx 配置只安装了一份。
  • /etc/<app>/<app>.env 已经按生产环境填写。

常用命令:

1
2
3
4
git status --short
git log -1 --oneline
sudo nginx -t
systemctl status <app>

硬性约束

  • 所有 shell 脚本必须使用 LF 行尾。
  • .gitattributes 必须包含 *.sh text eol=lf
  • 可执行脚本建议提交执行位,不要在部署时临时 chmod 弄脏工作区。
  • deploy/*.example 不能包含真实密钥。
  • deploy-prod.sh 里所有危险删除操作必须有路径保护。
  • Nginx 配置只安装到一种 include 目录。
  • server_name _ 只适合兜底或测试,生产要改成真实域名。
  • install-runtime.sh 必须处理服务用户、目录 owner 和写权限。
  • 部署脚本失败必须立即退出,不能静默跳过关键步骤。
  • 每次发布后必须执行 smoke test。

可逐步优化项

第一阶段先保证可复制:

  • 固定目录结构。
  • 固定模板文件名。
  • 固定脚本职责。
  • 固定部署命令。

第二阶段提高安全性:

  • 关闭生产 Swagger。
  • 增加 Nginx HTTPS 模板。
  • 增加危险路径保护。
  • 增加服务用户和权限初始化。

第三阶段提高可恢复性:

  • 保留上一版 jar 和 dist。
  • 发布失败自动回滚。
  • 增加 journalctl -u <app> 失败提示。
  • 记录部署版本、commit 和时间。

第四阶段再考虑容器化:

  • 保留 env、端口、健康检查、Nginx 入口这些约定。
  • systemd 启动方式替换成容器启动方式。
  • 不改变应用配置模型。