上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:12小时前 阅读量:15
生产环境部署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 应通过访问日志或监控按时间窗口统计,不能凭连接数制造确定结论。

资源限制应按以下顺序处理:

  1. 观察正常时段和业务高峰的 CPU、内存、连接数、磁盘增长速度和日志增长速度。
  2. 确认应用实际并发能力、数据库连接上限以及代理和上游的超时关系。
  3. 只针对明确瓶颈设置 MemoryMax、LimitNOFILE 或进程并发上限。
  4. 设置后观察 OOM、连接耗尽、请求排队和频繁重启。
  5. 如果限制导致服务异常,先恢复原限制,再根据观测结果重新分析。

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 服务器生产部署,必须能分别验证反向代理、进程守护、日志、证书、备份和资源边界,并明确出现什么现象时停止修改、恢复到哪个状态。

目录结构
全文