生产环境部署Linux服务器要注意什么:反向代理、进程守护与备份边界

生产环境里的 Linux 服务器,不能只以“应用进程启动并返回 200”作为上线标准。更稳妥的目标状态是:公网请求先进入 Nginx 反向代理,HTTPS 在代理层终止,应用只监听 127.0.0.1,systemd 负责进程拉起与异常重启;日志有可查询和可限制的去向,证书能够续期,备份覆盖配置、密钥和业务数据,并且变更失败时能恢复到最近一次可用状态。
实际故障往往来自边界没有划清:应用端口直接暴露在公网、进程退出后没人拉起、日志写满磁盘、证书到期,或者备份和生产数据位于同一块磁盘。下面以使用 apt 和 systemd 的 Debian/Ubuntu 类 Linux 服务器为例,假设应用运行在 127.0.0.1:8000,站点域名为 example.com。域名、端口、启动命令、目录和服务名都必须替换为当前环境的真实值。
一、先确认现状、影响范围和回滚材料
1. 变更前置条件
开始部署前,应确认:
- 具备服务器管理权限,并保留一个未关闭的 SSH 会话作为应急通道。
- 域名 DNS 已指向当前服务器,或已经准备好切换记录。
- 应用已经在测试环境验证过,能够在本机端口启动。
- 已明确应用的启动命令、工作目录、环境变量、静态文件目录和持久化数据目录。
- 已确认系统是否已经使用 Nginx、Apache、Docker、Supervisor 或其他进程管理方式。
- 已确定低峰期、变更负责人、观察人员和回滚负责人。
- 已有可读取的备份,至少包括现有代理配置、应用配置和必要的密钥材料。
如果应用已经由 Docker Compose、Supervisor 或其他工具管理,不要再为同一个进程叠加一套 systemd 守护。重复管理可能导致重复启动、端口争用和状态判断失真。生产环境中,一个应用进程最好只有一个明确的生命周期管理者。
2. 只读核对系统和监听状态
以下命令适用于 Debian/Ubuntu 类 Linux 系统,主要用于读取现状,不会主动修改配置:
cat /etc/os-release
uname -a
sudo ss -lntp
sudo systemctl --type=service --state=running
free -h
df -h
df -ih
uptime
重点核对:
127.0.0.1:8000是否已有应用监听。若没有,Nginx 配置正确时仍会返回502 Bad Gateway。80和443是否已被其他 Web 服务占用。- 磁盘空间和 inode 是否接近耗尽。日志写满磁盘后,应用可能无法创建临时文件、写入上传内容或更新数据库。
- 是否已有同名站点配置、证书、定时任务或旧版本服务。
- 应用是否监听
0.0.0.0:8000。如果没有必要对外提供该端口,后续应改为只监听回环地址。
保存变更前状态时,不要只复制一个 Nginx 文件。配置、服务定义和监听信息应一起留档:
sudo install -d -m 0700 /var/backups/pre-deploy
sudo nginx -T > /var/backups/pre-deploy/nginx-before.txt 2>&1 || true
sudo systemctl cat myapp.service > /var/backups/pre-deploy/myapp-before.service 2>&1 || true
sudo ss -lntp > /var/backups/pre-deploy/listen-before.txt
sudo cp -a /etc/nginx /var/backups/pre-deploy/nginx-before
sudo cp -a /etc/systemd/system /var/backups/pre-deploy/systemd-before
myapp.service 是示例服务名。如果服务尚未建立,systemctl cat 报未找到服务是正常现象。备份目录可能包含环境变量、密钥或证书,必须限制权限,不能放在公共目录,也不能提交到代码仓库。
二、先建立“公网入口—代理—应用”的边界
推荐的请求链路是:
客户端
-> 80/443 公网入口
-> Nginx 反向代理
-> 127.0.0.1:8000 应用进程
-> systemd 负责进程生命周期
Nginx 负责公网入口、TLS 和请求转发,应用负责业务处理,systemd 负责应用进程的启动、停止和异常重启。这样发生故障时,可以区分问题是在 DNS、网络入口、代理、应用还是进程守护层,而不是一次性修改多个组件。
为应用准备独立用户和目录前,应确认当前部署没有使用同名用户或目录。下面的命令会创建系统用户和目录,属于权限变更,执行前要确认目标路径不存在需要保留的业务文件:
sudo useradd --system --home /nonexistent --shell /usr/sbin/nologin appuser 2>/dev/null || true
sudo install -d -o appuser -g appuser -m 0750 /opt/myapp/current
sudo install -d -o root -g appuser -m 0750 /etc/myapp
如果用户已经存在,应另外核对其 UID、主组和登录 shell,不能因为命令被 || true 忽略就认为用户状态符合预期。发布目录、运行时目录和持久化数据目录应分开;数据库文件、用户上传内容和密钥不能随着普通版本发布被覆盖。
环境变量文件可以先建立并收紧权限:
sudo touch /etc/myapp/myapp.env
sudo chown root:appuser /etc/myapp/myapp.env
sudo chmod 0640 /etc/myapp/myapp.env
数据库密码、第三方密钥和生产令牌不要写入 Nginx 配置或代码仓库。代理只需要知道应用地址和协议,应用机密应由应用环境变量或既有密钥管理方式读取。
三、配置反向代理:先验证 HTTP,再启用 HTTPS
1. 建立最小 HTTP 代理
如果系统尚未安装 Nginx,在 Debian/Ubuntu 类系统中可以执行:
sudo apt-get update
sudo apt-get install nginx
执行前应确认软件源可用、磁盘空间充足,并注意安装或升级可能触发服务启动或配置变化。系统级安装和升级不宜在业务高峰期直接执行。
先创建 ACME 验证目录:
sudo install -d -m 0755 /var/www/acme
建立站点配置:
sudo tee /etc/nginx/sites-available/example.com >/dev/null <<'EOF'
server {
listen 80;
listen [::]:80;
server_name example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
default_type text/plain;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
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 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
EOF
启用前先检查同名链接。若目标已经存在,不要直接覆盖,应先比较当前配置:
sudo ls -l /etc/nginx/sites-enabled/example.com
sudo diff -u \
/etc/nginx/sites-enabled/example.com \
/etc/nginx/sites-available/example.com
确认可以替换或目标不存在后,再建立链接并检查语法:
sudo ln -s /etc/nginx/sites-available/example.com \
/etc/nginx/sites-enabled/example.com
sudo nginx -t
sudo systemctl reload nginx
nginx -t 失败时不要执行 reload。配置测试通过后,reload 通常不会立即中断已有连接,但错误配置可能使新请求无法处理。
先从服务器本机验证代理:
curl -i -H 'Host: example.com' http://127.0.0.1/
若返回 502,先检查应用,不要反复修改 Nginx:
curl -i http://127.0.0.1:8000/
sudo ss -lntp | grep ':8000'
结果可以这样判断:
- 直接访问
127.0.0.1:8000也失败:应用未启动、监听地址错误或应用自身报错。 - 直接访问成功,但 Nginx 返回
502:检查proxy_pass、协议类型、应用监听端口、权限和 Nginx 错误日志。 - 代理返回应用的
404:请求已经到达应用,通常应检查应用路由或Host处理,而不是先修改进程守护。
2. 签发并启用 HTTPS
使用 Webroot 方式签发证书时,域名解析必须到达当前服务器,公网入口还必须能够访问 80 端口。云防火墙和主机防火墙应按现有变更流程确认放行,不要为了排障无条件关闭防火墙。
先核对 Certbot 是否已经安装。软件包名称和来源可能随发行版变化,不确定时先查看版本:
certbot --version
签发证书:
sudo certbot certonly \
--webroot \
-w /var/www/acme \
-d example.com
签发后核对证书文件和权限:
sudo certbot certificates
sudo ls -l /etc/letsencrypt/live/example.com/
再将站点调整为 HTTP 跳转 HTTPS,同时保留 ACME 验证路径:
sudo tee /etc/nginx/sites-available/example.com >/dev/null <<'EOF'
server {
listen 80;
listen [::]:80;
server_name example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
default_type text/plain;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
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 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
EOF
sudo nginx -t
sudo systemctl reload nginx
应用是否信任 X-Forwarded-Proto,要根据框架的受信任代理配置决定。只有来自受信任反向代理的请求才应被应用当作 HTTPS,不能简单信任公网客户端自行提交的同名请求头。
验证跳转、证书和 SNI:
curl -I http://example.com/
curl -I https://example.com/
openssl s_client \
-connect example.com:443 \
-servername example.com /dev/null |
openssl x509 -noout -subject -issuer -dates
如果本机 DNS 尚未切换,应从外部测试机执行验证。服务器本机回环测试不能代替对公网 DNS、证书链和入口网络的检查。
四、用 systemd 守护应用,并让日志可查询
1. 先用目标用户确认启动命令
不要猜测应用的启动方式。应在应用目录中使用目标运行用户手工验证一次:
sudo -u appuser \
env APP_ENV=production \
/opt/myapp/bin/start
/opt/myapp/bin/start 只是示例,必须替换为实际启动脚本或可执行文件。手工启动时应确认应用能够绑定 127.0.0.1:8000;在手工进程退出前,不要同时启动同一个服务,否则容易产生端口占用。
2. 创建并验证服务单元
sudo tee /etc/systemd/system/myapp.service >/dev/null <<'EOF'
[Unit]
Description=My application
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp/current
EnvironmentFile=-/etc/myapp/myapp.env
ExecStart=/opt/myapp/bin/start
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s
NoNewPrivileges=true
PrivateTmp=true
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
NoNewPrivileges 和 PrivateTmp 可能影响依赖提权或共享临时目录的旧应用。启用后应结合应用测试保留;如果应用因此无法启动,应先查看失败原因并调整对应单元,不要直接关闭全部安全限制。
加载、启用并启动:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
检查状态和日志:
sudo systemctl is-enabled myapp.service
sudo systemctl is-active myapp.service
sudo systemctl status myapp.service --no-pager
sudo journalctl -u myapp.service -n 100 --no-pager
确认应用没有意外暴露到公网:
sudo ss -lntp | grep ':8000'
理想结果是应用监听 127.0.0.1:8000,而不是 0.0.0.0:8000。如果框架只能通过启动参数设置监听地址,应将参数写入启动脚本或环境文件,然后重启:
sudo systemctl restart myapp.service
若服务进入启动失败或频繁重启状态,应先查看 journalctl,不要连续执行 restart 制造更多噪声。
3. 给日志和证书续期设置边界
应用标准输出和错误输出已经交给 journald,可以按服务和时间查询:
sudo journalctl -u myapp.service --since "30 minutes ago" --no-pager
sudo journalctl -u nginx.service --since "30 minutes ago" --no-pager
sudo journalctl --disk-usage
如果需要限制 journald 占用空间,应先查看现有配置和磁盘用途。下面是配置示例,具体容量和保留周期应依据排障、审计要求及磁盘容量决定:
sudo install -d -m 0755 /etc/systemd/journald.conf.d
sudo tee /etc/systemd/journald.conf.d/limits.conf >/dev/null <<'EOF'
[Journal]
SystemMaxUse=1G
MaxRetentionSec=14day
EOF
sudo systemctl restart systemd-journald
sudo journalctl --disk-usage
Nginx 访问日志和错误日志通常由系统的 logrotate 管理,先检查配置,不要盲目强制轮转:
sudo ls -l /etc/logrotate.d/nginx
sudo logrotate -d /etc/logrotate.conf
日志中可能出现 URL 参数、用户标识或敏感信息。日志备份和集中采集应受权限控制并按要求脱敏。
确认证书续期定时任务:
sudo systemctl list-timers --all | grep -Ei 'certbot|letsencrypt'
Certbot 管理的证书可以先执行演练:
sudo certbot renew --dry-run
如果续期成功后需要自动让 Nginx 重新加载证书,可以建立部署钩子:
sudo install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
set -eu
/usr/sbin/nginx -t
/bin/systemctl reload nginx
EOF
sudo chmod 0755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
续期失败时,按顺序检查续期工具日志、域名解析、80/443 入口、Nginx 配置和验证目录权限。不要删除 /etc/letsencrypt 后重新申请,这会丢失现有续期配置,也可能造成不必要的申请频率问题。
五、划清备份内容和资源边界
1. 备份必须对应恢复目标
生产环境需要区分“恢复配置”和“恢复业务数据”:
| 内容 | 是否应备份 | 恢复时的核验 |
|---|---|---|
| Nginx 配置 | 是 | 执行 nginx -t 后再 reload |
| systemd 服务文件 | 是 | daemon-reload 后再启动 |
| 应用环境变量和密钥 | 是 | 加密保存并严格限制读取权限 |
| 应用发布包、依赖锁定文件 | 通常应备份 | 确认与配置、数据版本匹配 |
| 数据库 | 是 | 使用一致性备份或数据库原生备份工具 |
| 用户上传内容 | 按业务需要 | 核对权限、路径和完整性 |
| 缓存、临时文件 | 通常不必备份 | 恢复后由应用重新生成 |
| 普通访问日志 | 按审计要求 | 核对隐私、容量和留存周期 |
数据库正在写入时,不能简单复制其数据目录来代替一致性备份。应使用对应数据库的备份工具或存储快照机制,并把恢复步骤一并记录。
归档配置时,输出文件可能包含密钥和证书,应放在受限目录:
backup="/var/backups/myapp-$(date +%Y%m%d-%H%M%S).tar.gz"
sudo tar \
--xattrs \
--acls \
-czf "$backup" \
/etc/nginx \
/etc/systemd/system/myapp.service \
/etc/myapp \
/etc/letsencrypt
sudo chmod 0600 "$backup"
sudo tar -tzf "$backup" >/dev/null
这份归档适合恢复配置,不等于完整灾备。如果归档和生产服务器位于同一块磁盘,磁盘损坏、误删或主机不可用时可能同时丢失。备份至少应复制到与生产主机故障域不同的位置,并确认传输加密、访问权限和保留策略。
恢复前先在临时目录验证归档,不要未经检查就覆盖生产目录:
sudo install -d -m 0700 /tmp/myapp-restore-check
sudo tar -xzf /var/backups/myapp-YYYYMMDD-HHMMSS.tar.gz \
-C /tmp/myapp-restore-check
sudo find /tmp/myapp-restore-check -maxdepth 4 -type f | head
恢复时先保留当前版本,再逐项恢复配置、检查语法并观察服务状态。密钥、配置和数据库要分别确认;旧应用版本是否兼容当前数据结构,不能只凭文件版本号判断。
2. 资源边界要区分连接数和请求速率
反向代理、应用进程和日志系统会共同消耗 CPU、内存、磁盘、inode、文件描述符和连接数。先建立基线:
free -h
df -h
df -ih
uptime
sudo systemctl show myapp.service \
-p MemoryCurrent \
-p TasksCurrent \
-p NRestarts
sudo ss -s
并发连接数不是每秒请求数。连接数表示某一时刻仍保持的连接数量,RPS 表示单位时间内完成或进入的请求数量,不能用 ss -s 的连接数直接替代 RPS。在稳定且近似的情况下,可以用:
并发请求数 ≈ RPS × 平均请求耗时(秒)
这个关系只能作为容量判断的粗略参考,长连接、排队、突发流量和不同请求耗时都会使结果偏离;真正的 RPS 应通过访问日志或监控按时间窗口统计,不能凭连接数制造确定结论。
资源限制应按以下顺序处理:
- 观察正常时段和业务高峰的 CPU、内存、连接数、磁盘增长速度和日志增长速度。
- 确认应用实际并发能力、数据库连接上限以及代理和上游的超时关系。
- 只针对明确瓶颈设置
MemoryMax、LimitNOFILE或进程并发上限。 - 设置后观察 OOM、连接耗尽、请求排队和频繁重启。
- 如果限制导致服务异常,先恢复原限制,再根据观测结果重新分析。
MemoryMax 过小可能使应用被内核或 cgroup 终止;文件描述符过低会导致新连接失败;代理超时时间过长则可能长期占用连接。它们都不是越大越好。
六、按由外到内的顺序验证,并保留回滚路径
变更完成后,先验证配置和服务,再验证公网入口,最后验证应用关键业务:
sudo nginx -t
sudo systemctl is-active nginx
sudo systemctl is-active myapp.service
curl -I http://example.com/
curl -I https://example.com/
curl -i http://127.0.0.1:8000/
sudo journalctl -u nginx.service -n 50 --no-pager
sudo journalctl -u myapp.service -n 50 --no-pager
常见结果的判断方式如下:
| 现象 | 优先检查位置 | 处理方向 |
|---|---|---|
| HTTP 无法连接 | DNS、入口端口、防火墙、Nginx 状态 | 先确认请求是否到达服务器 |
| HTTPS 证书错误 | 证书路径、域名、有效期、SNI | 用 openssl s_client 核对实际返回证书 |
Nginx 返回 502 | 应用监听、端口、协议和应用日志 | 先直接访问 127.0.0.1:8000 |
| 应用不断重启 | 启动命令、环境变量、权限和依赖 | 查看 journalctl -u myapp.service |
| 日志写满磁盘 | journald、Nginx 日志、应用日志 | 先保留必要日志,再按策略清理和限制 |
| 续期演练失败 | ACME 路径、DNS、80 端口和续期配置 | 不删除现有证书目录,逐项修复后重试 |
关键业务请求也要验证,例如登录、上传、后台任务或依赖外部服务的请求。单个首页返回 200,不能证明完整业务链路可用。
变更后不要立即关闭旧 SSH 会话,也不要马上删除旧版本和旧配置。观察期间至少关注:
- HTTP 到 HTTPS 的跳转和 HTTPS 请求是否持续正常。
myapp.service是否保持active,NRestarts是否异常增长。- Nginx 是否持续出现
502、连接超时或权限错误。 - 内存、磁盘、inode 和文件描述符是否接近边界。
- 实际返回的证书内容是否正确。
- 关键业务请求和后台任务是否异常。
满足以下任一条件,应优先回滚,而不是继续叠加修改:
- 新配置导致
nginx -t失败,或 reload 后公网请求大面积失败。 - 应用持续启动失败、反复重启或无法处理关键业务。
- HTTPS 证书错误且无法在观察窗口内修复。
- 磁盘、内存或连接数快速耗尽,故障范围正在扩大。
- 备份无法读取,或无法证明配置和数据可以恢复。
Nginx 配置回滚前先保存当前失败版本,再恢复已验证的旧配置:
sudo nginx -t
sudo systemctl reload nginx
systemd 服务文件回滚后执行:
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
sudo systemctl status myapp.service --no-pager
如果应用使用版本目录或发布包,应切回上一份已验证的发布内容,而不是直接删除当前目录。配置、密钥和数据库恢复必须分别确认;旧应用版本恢复后,还要检查其与当前数据结构是否兼容。
观察窗口结束后,再清理临时文件、停用旧发布并归档变更记录。一个可交付的 Linux 服务器生产部署,必须能分别验证反向代理、进程守护、日志、证书、备份和资源边界,并明确出现什么现象时停止修改、恢复到哪个状态。