本地跑得再顺的 webserver,一旦要放到公网上给别人访问,问题就会像串门一样一个接一个冒出来:进程半夜挂了、端口开了但外面连不上、域名解析了却打不开、HTTPS 证书过期了没人管、安全组规则写错把自己锁在外面。我这几年帮人做过不少“把本地项目搬到公网”的活,从个人小工具、内部管理系统,到给朋友做的展示型站点,绕来绕去其实都是同一套东西:一台云服务器(弹性计算实例或轻量应用服务器这类)、一个能常驻的进程管理方案、一层反向代理、一份能让外部访问的域名与证书配置,再加上一套不至于让自己裸奔的安全策略。下面这篇就把这套流程完整拆一遍,从选机器到上线验证,把参数计算、配置细节和踩过的坑都摊开讲,适合刚把项目在本地跑通、准备第一次上公网的朋友,也适合已经上线但总觉得“哪里不太对劲”的人对照着自查。
1. 部署方案的整体设计与选型考量
1.1 从“本地能跑”到“公网可访问”到底差了什么
很多人对部署的理解停留在“把代码扔上去,跑起来就行”,但本地环境和公网环境之间隔着好几层东西,每一层都可能成为拦路虎。本地开发时,你是用localhost:3000或者127.0.0.1:8000访问的,这个地址只有你自己这台机器认识;一旦别人要从外部访问,请求会先经过 DNS 解析、公网路由、云厂商的网络边界(安全组)、操作系统的防火墙、监听地址绑定,最后才落到你的应用进程上。这条链路上任何一环没打通,表现都是“打不开”,但原因可能天差地别。
我习惯把这套链路的差异归纳成四类:可达性差异(外网能不能找到你)、持续性差异(进程能不能一直活着)、安全性差异(暴露在公网后攻击面有多大)、可维护性差异(出问题你怎么知道、怎么修)。本地开发完全不用考虑前两项,第三项压根不存在,第四项靠 IDE 的终端日志就够了。上了公网,这四项全都要重新设计一遍。
所以我会在动手之前先把这几个问题回答清楚:这个服务是给自己用还是给别人用?访问量大概什么量级?有没有需要长期保存的数据?能不能接受几分钟的停机?这些答案直接决定了后面选什么规格的机器、要不要上数据库、要不要做备份、日志留多久。很多人跳过这一步直接买机器,结果要么买大了浪费钱,要么买小了天天被 OOM 杀掉进程。
1.2 服务器规格、系统镜像与带宽怎么选
规格选择这块,我的经验是先按内存卡下限,再按带宽卡上限。绝大多数 web 服务是内存敏感型而不是 CPU 敏感型:一个 Node 进程空跑也要占 80-150MB,Python 的 gunicorn 每个 worker 大概 60-120MB,再加上系统本身占的 200MB 左右,2GB 内存基本就是“能跑但不宽裕”的分界线,1GB 在装完运行时和反向代理之后经常剩不下什么,稍微来点并发就开始用 swap,swap 一上,响应时间立刻拉胯。
给个粗略的估算方法:假设你用 Nginx 做反向代理,应用进程开 2 个 worker,每个 worker 峰值内存 150MB,那么内存下限 ≈ 系统 200MB + Nginx 30MB + 应用 300MB + 缓冲余量 300MB ≈ 850MB,取整到 1GB 是紧巴巴的,2GB 才舒服。CPU 方面,这类小站 2 核足够,1 核也能跑,只是编译依赖、打包、证书签发这些操作会慢一些。
带宽是最容易被低估的一项,也是最容易产生额外费用的地方。这里有个换算要记牢:1Mbps 带宽的理论下载速度是 128KB/s,实际跑起来扣掉协议开销大概 110-120KB/s。假设你的页面首屏资源(HTML + CSS + JS + 图片)总共 500KB,那么在 1Mbps 带宽下,一个用户完整加载需要 4 秒多,而且这 4 秒内带宽是被占满的,第二个人同时访问就得排队。所以如果页面做了压缩、图片控制在 100KB 以内、总资源 200KB 左右,1-3Mbps 的固定带宽撑住每天几百次访问没问题;如果页面里有大图、视频、或者要提供文件下载,那按流量计费或者直接用对象存储放静态资源会更划算。
| 场景类型 | 建议内存 | 建议 CPU | 带宽策略 | 说明 |
|---|---|---|---|---|
| 个人工具 / 演示站 | 2GB | 2 核 | 固定 1-3Mbps | 访问量低,静态资源少,固定带宽成本可控 |
| 内部管理系统 | 2-4GB | 2 核 | 固定 3-5Mbps | 并发用户十几个,注意数据库内存占用 |
| 含图片/附件上传的站点 | 4GB | 2 核 | 按流量计费 + 对象存储 | 附件放对象存储,别让服务器扛流量 |
| 有持续写入的业务 | 4GB+ | 2-4 核 | 固定 5Mbps 起 | 需要额外规划数据盘和备份 |
系统镜像我一般直接选长期支持版本的 Linux 发行版,Ubuntu LTS 或者 Debian 稳定版都行,社区资料多、软件源新、出问题好搜。不建议为了“熟悉”选桌面版镜像,服务器上多一个图形界面就是多几百 MB 内存和一堆用不上的服务,纯属给自己增加攻击面。
注意:买机器时如果选了“按量付费”的带宽,一定去控制台把带宽上限和费用告警设好,我就见过有人忘了设上限,被一波异常流量刷出一个月饭钱的账单。
1.3 部署拓扑与安全边界的设计原则
拓扑这块,小项目最容易犯的错是“把应用端口直接暴露到公网”。比如 Node 服务监听 3000,就去安全组开 3000 端口,然后让用户用http://IP:3000访问。这样能通,但代价是:没有 HTTPS、没有统一的访问日志、应用直接面对公网请求(框架层面的漏洞会被直接打到)、以后想换端口或者加个路径前缀就得通知所有人。更麻烦的是,很多框架的开发服务器在生产模式下压根不该用,性能和安全都不达标。
我推荐的拓扑是三层收敛:公网只开放 80 和 443(以及一个改过端口的 SSH),Nginx 监听这两个端口做反向代理,应用进程只监听127.0.0.1:端口,完全不对外。这样做的收益很实在:请求进来先过 Nginx,限流、缓存、压缩、日志、证书全在这一层解决;应用挂了换个端口重启,外部完全无感;应用即使有漏洞,攻击者也得先突破 Nginx 这一层;将来加第二个服务,只是多一个location的事。
公网用户 | (80 / 443) v 云厂商安全组(只放行必要端口) | v 系统防火墙(与安全组规则保持一致) | v Nginx 反向代理(TLS 终止、静态资源、限流、日志) | v (127.0.0.1:3000,仅本机可达) 应用进程(由 systemd 守护)这个结构还有个隐性好处:它逼着你把配置和应用解耦。应用不需要知道自己跑在哪个域名下,也不用处理证书,这些都在 Nginx 层配置。以后从一台机器迁到另一台、或者加个负载均衡,改动量都很小。
2. 服务器初始化与运行环境搭建
2.1 首次登录与最小权限账号配置
新机器到手第一件事不是装环境,是把登录这件事弄稳妥。控制台给的初始方式通常是密钥对或者初始密码,不管哪种,第一步都要创建一个普通用户,后续所有操作都用它,需要提权时再sudo。原因很直白:root 是所有人的第一猜测对象,直接拿 root 对外,等于把用户名这一半的信息白送出去。
# 以 root 或初始账号登录后执行 adduser deploy usermod -aG sudo deploy # 把公钥复制过去,实现免密登录 mkdir -p /home/deploy/.ssh chmod 700 /home/deploy/.ssh cp /root/.ssh/authorized_keys /home/deploy/.ssh/ chmod 600 /home/deploy/.ssh/authorized_keys chown -R deploy:deploy /home/deploy/.ssh搞定之后先别急着关掉当前会话,另开一个终端窗口用新账号登录验证一遍,确认能上去、能sudo,再去动 SSH 配置。这个习惯救过我两次,一次是密钥文件权限不对导致验证失败,一次是配置写错把端口改成了被占用的值,因为老会话还开着,才不至于彻底进不去。
登录上去之后顺手做几件小事:timedatectl set-timezone Asia/Shanghai把时区调对(日志时间差 8 小时排查起来非常难受)、hostnamectl set-hostname web-prod-01设置主机名(多台机器时一眼能分清)、apt update && apt upgrade -y把系统补丁打一遍。这些都不是必须的,但做完之后整个环境会“干净”很多。
2.2 系统基础组件与软件源优化
接下来是把常用的基础工具装上。不要小看这一步,很多排查问题的关键时刻,就是靠curl、netstat、dig、vim这些工具救场。
sudo apt install -y curl wget git vim unzip htop \ net-tools dnsutils ufw fail2ban ca-certificates gnupgnet-tools提供netstat,dnsutils提供dig和nslookup,fail2ban后面做 SSH 防爆破要用。有些精简镜像默认没装curl,连安装脚本都跑不了,先补上这些没坏处。
软件源方面,默认源有时候速度不理想,可以换成地理位置更近的镜像源。但这里有个坑要提醒:换源之后一定要再跑一次apt update,确认没有 GPG 报错。我遇到过一次换了源但密钥没同步,结果后续所有安装都失败,报错信息还很绕,排查了半小时才发现是源的问题。
安全更新这块,我建议开启自动安全更新,只更新安全补丁,不升级大版本。这样做的好处是不用天天盯着,坏处是偶尔会在半夜自动重启服务,所以如果你跑的是对重启敏感的服务,就改成手动检查、定期维护窗口更新。
sudo apt install -y unattended-upgrades sudo dpkg-reconfigure -plow unattended-upgrades2.3 运行时环境的安装与版本固定
运行时安装最忌讳的是“随手装个最新的”。版本漂移是线上事故的常见源头:本地 Node 18 跑得好好的,服务器装了个 22,某些依赖的 ABI 变了,或者某个 API 行为改了,报错信息还跟版本毫无关系。我的做法是显式固定版本,并且在项目里写清楚,让本地和线上保持一致。
以 Node 为例,用版本管理工具或者官方源都行,关键是记下版本号:
# 使用 NodeSource 源安装指定大版本 curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs node -v && npm -vPython 项目的话,系统自带的 Python 版本往往偏旧,建议用pyenv或者uv管理,虚拟环境一定要建,不要往系统 Python 里装包,不然哪天系统工具升级依赖冲突了,apt都能给你弄坏。
sudo apt install -y python3 python3-venv python3-pip cd /opt/myapp python3 -m venv venv source venv/bin/activate pip install -r requirements.txt装完之后做一次冒烟测试,确认运行时能正常起一个最小服务。这一步看似多余,但能提前把“缺系统库”“动态链接库版本不对”这类问题暴露出来,比等到部署完再排查省事得多。
2.4 代码上传与目录结构规划
代码怎么传到服务器,取决于你的工作流。用 Git 是最省心的,直接在服务器上git clone,以后更新就是git pull。私有仓库的话,建议用部署密钥(deploy key),只给读权限,比把个人账号的密钥放服务器上安全得多。
目录结构上,我习惯用/opt/放应用代码,/var/log/放日志,/etc/放配置,/var/www/放静态文件。这么分的原因是符合 Linux 的惯例,将来别人接手或者你自己半年后再看,一眼就知道东西在哪。
sudo mkdir -p /opt/myapp sudo chown -R deploy:deploy /opt/myapp git clone git@example.com:team/myapp.git /opt/myapp sudo mkdir -p /var/log/myapp sudo chown -R deploy:deploy /var/log/myapp依赖安装要在部署目录内做,别用全局安装。npm ci比npm install更适合线上,因为它严格按 lock 文件装,不会因为解析出新的小版本导致行为不一致。装完跑一次构建(如果有构建步骤),确认产物生成在预期位置。
提示:
.env这类配置文件不要提交到仓库,在服务器上单独创建,权限设成600,属主是运行应用的用户。数据库密码、密钥这些泄露一次就够喝一壶的。
3. Web 服务的常驻运行与进程守护
3.1 为什么不能直接 nohup 或开个 screen
我见过太多人用nohup node app.js &或者开个screen会话就跑上线了。这样确实能让进程在终端断开后继续活着,但问题在于:机器重启之后它不会自动起来,进程崩溃之后它不会自动拉起,日志没有任何轮转,出问题你只能靠手动ps去找。作为临时调试手段没问题,作为生产方案是不合格的。
进程守护要解决的核心问题有三个:自动重启(崩溃后几秒内拉起)、开机自启(服务器重启后无需人工干预)、日志归一(标准输出和错误输出有统一去处,能配合日志轮转)。这三件事 systemd 全都能做,而且是系统自带的,不用额外装 supervisor 之类的工具。
3.2 systemd 服务单元的完整配置
下面这份配置是我用得最多的模板,改改路径和命令就能用:
# /etc/systemd/system/myapp.service [Unit] Description=My Web Application After=network.target Wants=network-online.target [Service] Type=simple User=deploy Group=deploy WorkingDirectory=/opt/myapp Environment="NODE_ENV=production" Environment="PORT=3000" EnvironmentFile=-/opt/myapp/.env ExecStart=/usr/bin/node /opt/myapp/dist/server.js Restart=always RestartSec=3 StandardOutput=append:/var/log/myapp/app.log StandardError=append:/var/log/myapp/error.log # 安全加固 NoNewPrivileges=true PrivateTmp=true ProtectSystem=full ProtectHome=true [Install] WantedBy=multi-user.target几个参数值得展开说。Restart=always是核心,进程无论因为什么原因退出都会被拉起;RestartSec=3是重试间隔,别设太短,否则一个启动就崩的程序会疯狂重启把 CPU 打满;EnvironmentFile前面加个-表示文件不存在也不报错,避免因为漏了配置文件导致服务起不来;Wants=network-online.target配合After能减少“开机时网络还没就绪就启动然后失败”的概率。
安全加固那几行看起来不起眼,作用不小。NoNewPrivileges=true阻止进程通过 setuid 提权,PrivateTmp=true给进程一个独立的/tmp,ProtectSystem=full把/usr、/boot、/etc挂成只读(你自己的/opt目录不受影响),ProtectHome=true让进程看不到/home下的东西。这些配置基本不影响正常业务,但能显著降低被拿下之后的破坏半径。
启用流程:
sudo systemctl daemon-reload sudo systemctl enable myapp sudo systemctl start myapp sudo systemctl status myappenable和start是两件事,enable管开机自启,start管现在就跑,两个都要做。改完配置之后必须daemon-reload,不然 systemd 读的还是旧配置,这个坑我踩过不止一次,改了配置重启服务发现没生效,折腾半天才想起来。
3.3 日志管理与轮转策略
日志无限增长是老生常谈的问题,但真正出事的时候往往是磁盘满了才发现。一个高频报错的服务,一天写几个 G 的日志完全可能,20G 的系统盘撑不了几天。
两种做法,一是交给 systemd 的 journald 管理,用journalctl查;二是指定文件输出,用 logrotate 切割。我更倾向第二种,因为排查的时候tail -f和grep更顺手,也能直接把日志文件下载下来分析。
# /etc/logrotate.d/myapp /var/log/myapp/*.log { daily rotate 14 compress delaycompress missingok notifempty copytruncate }copytruncate这个参数很关键:它先复制一份日志文件再清空原文件,不需要程序重新打开文件句柄,适合那些不自己处理信号的应用。如果你用的是create模式(重命名后新建),程序必须支持收到信号后重开日志,否则会继续往那个已经被重命名的文件里写,新日志永远是空的。
rotate 14表示保留 14 份,按天切割就是两周。实际保留多久看你的排查需求和磁盘大小,但至少留 7 天,因为很多问题不是你当天就能发现的,等用户反馈过来可能已经过去三四天了。
4. 反向代理、域名解析与 HTTPS 配置
4.1 Nginx 反向代理的完整配置文件
Nginx 装好之后,配置不要直接改主文件,而是在sites-available里新建一个站点配置再软链过去,这样管理多个站点的时候清爽很多。
# /etc/nginx/sites-available/myapp.conf server { listen 80; listen [::]:80; server_name example.com www.example.com; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://$host$request_uri; } } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name example.com www.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; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN; add_header Referrer-Policy strict-origin-when-cross-origin; client_max_body_size 20m; gzip on; gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml image/svg+xml; gzip_min_length 1024; gzip_comp_level 5; access_log /var/log/nginx/myapp.access.log; error_log /var/log/nginx/myapp.error.log warn; location / { proxy_pass http://127.0.0.1:3000; 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_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_connect_timeout 10s; proxy_read_timeout 60s; proxy_send_timeout 60s; } location /static/ { alias /opt/myapp/public/static/; expires 7d; add_header Cache-Control "public, immutable"; access_log off; } }几个地方值得细说。proxy_set_header那四行是必须的,缺了X-Forwarded-For,你的应用日志里记录的全是127.0.0.1,完全没法做访问分析;缺了X-Forwarded-Proto,应用判断不出原始请求是 HTTP 还是 HTTPS,生成的重定向链接可能是错的。Upgrade和Connection这两行是为了 WebSocket 支持,如果你的应用有实时推送、在线状态之类的功能,缺了这两行连接会握手失败,而且报错信息很不直观。
client_max_body_size默认只有 1MB,文件上传超过 1MB 直接返回 413,这个报错在应用日志里看不到,只在 Nginx 的错误日志里有一行,非常容易被误判成后端问题。所以我一般直接按业务上限设一个明确的值。
gzip对文本类资源效果显著,JSON 接口响应 gzip 之后通常能省 60-70% 的流量,对带宽紧张的小机器帮助很大。但图片、视频这些已经是压缩格式的,再 gzip 只是浪费 CPU,所以gzip_types里不要加。
配置写完先测语法,再重载,别直接 restart:
sudo nginx -t sudo systemctl reload nginxreload是平滑重载,正在处理的请求不受影响;restart会断掉所有连接。线上环境养成用reload的习惯。
4.2 域名解析与接入前的准备工作
域名这块,第一步是把 A 记录指向服务器的公网 IP。解析生效有延迟,通常几分钟到几小时不等,取决于 TTL 设置。调试阶段可以先把 TTL 改成 60 秒,上线稳定之后再改回 600 秒以上,这样以后换 IP 的时候切换更快。
解析是否生效用dig验证,不要只信浏览器:
dig example.com +short dig www.example.com +short如果返回的不是你配置的 IP,先别怀疑服务器,八成是解析还没生效或者本地 DNS 缓存的问题。可以换一个公共 DNS 再查一次,用dig @8.8.8.8 example.com +short这样的写法,能快速判断是解析问题还是本地缓存问题。
还有一件容易被忽略的事:域名要在境内服务器上正常对外提供访问,通常需要先完成实名认证和接入相关的合规手续,这是个纯流程性的工作,跟技术没关系,但需要时间。我的建议是买域名和买服务器放在同一天做,把材料一次性准备好提交,等手续跑完的这段时间正好用来搭环境、调配置,两边并行,不浪费时间。很多人是环境都搭好了才发现域名还不能用,干等着。
4.3 免费证书的申请与自动续期
HTTPS 现在基本是标配,浏览器对 HTTP 站点的提示越来越明显。好在免费证书的申请已经完全自动化了,用certbot几分钟就能搞定。
sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d example.com -d www.example.com它会自动改 Nginx 配置、自动完成验证、自动配置续期。执行完可以测试一下续期流程是否正常:
sudo certbot renew --dry-run这一步很重要,很多人只管申请不管续期,三个月后证书过期,浏览器直接红屏警告。--dry-run会模拟一次续期,把可能的问题(比如验证路径被拦截、80 端口不通)提前暴露出来。certbot 装完之后会自动创建定时任务,每天检查两次,剩余 30 天内自动续期,正常情况下不需要你操心。
注意:续期验证默认走 80 端口的 HTTP 挑战,如果你的安全组或者防火墙把 80 关了,续期就会失败而且不会有任何提示,等发现的时候证书已经过期了。所以要么保持 80 端口对特定路径可达,要么改用其他验证方式。
5. 安全加固与访问控制策略
5.1 安全组与系统防火墙的双层控制
这里有个概念一定要分清:云厂商的安全组和系统防火墙是两套独立的规则,两套都要通才能访问。安全组在云平台网络层生效,流量压根到不了你的机器;系统防火墙在操作系统层生效,流量到了机器门口被拦下。我遇到过配置了系统防火墙但忘了改安全组的情况,也遇到过反过来,两边对了半天才发现另一边没开。
原则是两边规则保持一致,只开必要端口。最小集合大概是:
| 端口 | 协议 | 来源 | 用途 | 是否必须 |
|---|---|---|---|---|
| 22 | TCP | 你的固定 IP(如果可能) | SSH 管理 | 必须,建议限制来源 |
| 80 | TCP | 0.0.0.0/0 | HTTP 跳转 + 证书验证 | 必须 |
| 443 | TCP | 0.0.0.0/0 | HTTPS | 必须 |
| 应用端口 | TCP | 仅本机 | 内部通信 | 不对外开放 |
系统防火墙用ufw的话配置很简单:
sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow 22/tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable sudo ufw status verbose执行ufw enable之前一定要确认 22 端口已经放行,否则 SSH 立刻断掉,只能去控制台用 VNC 救。这个操作我建议在控制台开着的情况下做,心里踏实。
5.2 SSH 加固的几项关键配置
SSH 是暴露在公网上最危险的入口,全世界都在扫描。默认配置下,每天几千次爆破尝试是常态。加固主要做三件事:禁密码、改端口、上防爆破。
# /etc/ssh/sshd_config.d/99-hardening.conf Port 2222 PasswordAuthentication no PermitRootLogin no PubkeyAuthentication yes MaxAuthTries 3 LoginGraceTime 20 ClientAliveInterval 300 ClientAliveCountMax 2改端口的作用不是“更安全”,而是把日志里的噪音降低 95%,让你能一眼看出真正的异常尝试。这个是从业者的共识:改端口不增加实质安全性,但显著提升可维护性。
禁密码登录是真正的关键项。只要密码登录开着,弱密码就是早晚的事。改用密钥登录之后,爆破就彻底没戏了。配置改完先sudo sshd -t测语法,再用新端口开一个新会话验证,确认没问题再关旧会话。
防爆破用 fail2ban,它监控日志,发现同一个 IP 连续失败多次就临时封禁:
# /etc/fail2ban/jail.local [sshd] enabled = true port = 2222 maxretry = 5 findtime = 600 bantime = 3600maxretry = 5表示 10 分钟内失败 5 次就封 1 小时。这个阈值不要设得太低,自己手抖输错几次被自己封了就尴尬了。
5.3 常见攻击面的收敛与日常检查
除了 SSH,还有几个地方经常被忽视。
应用层的管理后台。如果你有/admin这类路径,别直接对公网开放。最简单的办法是在 Nginx 层加一层 IP 白名单:
location /admin/ { allow 203.0.113.10; deny all; proxy_pass http://127.0.0.1:3000; }接口的限流。Nginx 自带限流模块,几行配置就能挡住基本的刷接口:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; location /api/ { limit_req zone=api burst=20 nodelay; proxy_pass http://127.0.0.1:3000; }10r/s 的意思是每个 IP 每秒 10 个请求,burst 20 表示允许瞬时 20 个的突发。对小站来说这个阈值足够宽松,不会误伤正常用户,但能挡住大部分脚本化的恶意请求。
日常检查清单。我习惯每周花五分钟看几件事:df -h看磁盘(日志和数据最容易撑爆)、free -m看内存(有没有异常增长)、systemctl status看服务状态(有没有意外重启)、tail -100看错误日志(有没有新的异常)。这几项花不了几分钟,但能在问题变大之前发现苗头。
6. 上线验证与故障排查实录
6.1 分层排查:从外到内的定位方法
访问不通的时候最忌讳乱试,一通瞎改往往把原本好的地方也弄坏了。我习惯按链路的顺序从外往里查,每一步都能明确排除一层。
第一层,解析是否正常。dig example.com +short,返回的 IP 对不对。不对就是 DNS 问题,跟服务器无关。
第二层,端口是否可达。从本地机器测:
# 测端口通不通(没有 nc 的话用 telnet) nc -zv example.com 443 nc -zv example.com 80 # 直接看 HTTP 响应头,不依赖浏览器 curl -I https://example.com curl -v https://example.com 2>&1 | head -30curl -v能看到完整的握手过程,卡在哪一步一目了然:连接超时是网络层问题(安全组、防火墙),连接被拒是服务没监听,SSL 报错是证书问题,能连上但返回 502 是后端应用的问题。
第三层,服务是否在监听。在服务器上执行:
ss -lntp | grep -E '80|443|3000'这里要重点确认应用的监听地址,如果是0.0.0.0:3000说明对全网开放(不符合我们的设计),如果是127.0.0.1:3000才是正确的。
第四层,本机能否访问。绕过所有外部因素,在服务器上直接请求:
curl -I http://127.0.0.1:3000 curl -I -H "Host: example.com" http://127.0.0.1本机通而外网不通,问题一定在网络层或者 Nginx;本机都不通,那就是应用本身的问题,去看journalctl -u myapp -n 50或者应用的错误日志。
第五层,看日志。Nginx 的error.log里通常有直接答案,connect() failed (111: Connection refused)是后端没起,upstream timed out是后端太慢,no live upstreams是健康检查全挂了。
6.2 常见问题速查表
下面这些是我实际遇到过、并且被问得最多的,按现象整理成表格,方便对照:
| 现象 | 最可能的原因 | 验证方法 | 解决方向 |
|---|---|---|---|
| 浏览器显示连接超时 | 安全组未放行 / 防火墙拦截 | nc -zv IP 端口 | 检查安全组和ufw status |
| 连接被拒绝 | 服务未启动 / 监听地址不对 | ss -lntp | 检查 systemd 状态和监听绑定 |
| 502 Bad Gateway | 后端进程挂了或端口不对 | curl 127.0.0.1:端口 | 看应用日志,核对 proxy_pass |
| 504 Gateway Timeout | 后端处理太慢 | 看 Nginx error.log 耗时 | 优化查询或调proxy_read_timeout |
| 上传文件报 413 | client_max_body_size太小 | 看响应码和 error.log | 调大该参数并 reload |
| 静态资源 404 | 路径映射错误 / 权限不足 | ls -l看目录权限 | 检查alias结尾斜杠和属主 |
| 证书报过期 | 自动续期失败 | certbot renew --dry-run | 检查 80 端口和验证路径 |
| 服务隔几天就挂 | 内存不足被 OOM | `dmesg | grep -i oom` |
| 日志时间差 8 小时 | 时区未设置 | timedatectl | set-timezone Asia/Shanghai |
| 修改配置不生效 | 忘了 daemon-reload / reload | 看服务启动时间 | daemon-reload+restart |
6.3 几个印象深刻的踩坑记录
坑一:alias结尾的斜杠。静态资源 404 排查了两个小时,最后发现是location /static/配了alias /opt/myapp/public/static;,少了结尾的斜杠,导致路径拼接错位。alias和root的路径拼接规则不一样,alias是替换匹配部分,结尾斜杠必须和location保持一致,这个细节文档里写了但很容易看漏。
坑二:EnvironmentFile里的引号。.env文件里写了PASSWORD="abc#123",结果应用拿到的密码是被截断的。原因是部分解析器会把#当注释起始符,加上引号也没用。解决办法是密码里避开特殊字符,或者改用不同的配置加载方式。这类问题最恶心的地方是本地开发环境和线上表现不一致,因为本地可能压根没走同一个加载路径。
坑三:磁盘被 Docker 日志吃满。一个跑了三个月的服务突然全部挂掉,df -h显示 100%,查下来是容器日志没有任何限制,单个日志文件涨到了十几 G。教训是:任何会产生日志的组件,都要设上限。Docker 可以在 daemon 配置里设max-size和max-file,其他服务用 logrotate,总之不能放任。
坑四:reload之后新配置没生效。改了 Nginx 配置,nginx -t通过,reload也执行了,但行为没变。最后发现是同一个server_name在另一个配置文件里也定义了,Nginx 按字母序加载,实际生效的是另一个。排查这类问题的办法是nginx -T,它会打印合并后的完整配置,能直接看到最终生效的内容。
坑五:改了 SSH 端口但安全组没加。这个最典型,改完端口重启 sshd,当前会话还连着所以没感觉,下次登录就连不上了。只能去控制台用网页终端登进去改回来。所以改 SSH 端口的时候,先加安全组规则,再改配置,最后验证,顺序不能乱。
6.4 上线后第一周的观察重点
服务刚上线那几天是最关键的观察期,很多问题只在真实流量下才会暴露。我会重点盯这几个指标:响应时间的变化趋势(有没有随时间变慢,可能是内存泄漏或连接池耗尽)、错误率的分布(是集中在某个接口还是全局)、资源的增长曲线(内存是不是只涨不降)、访问来源的异常(有没有大量来自单一 IP 的请求)。
如果条件允许,配一个简单的监控比什么都强。不需要多复杂的方案,一个定时脚本每分钟记录一次内存、磁盘、响应码分布,写到文件里,出问题的时候至少有历史数据可查。我吃过没有监控的亏:服务凌晨三点挂了,早上七点才发现,中间四个小时的日志全是空的,完全不知道挂之前发生了什么。
7. 长期运维的成本与可维护性考量
7.1 带宽、流量与费用的控制思路
云服务器的费用结构里,带宽往往比机器本身还贵。固定带宽包月看起来简单,但如果选了 10Mbps 而实际只用 1Mbps,那 90% 的钱是白花的;反过来选小了,高峰期卡顿又影响体验。
我的做法是先按流量计费跑两周,看真实的流量曲线,再决定要不要改成固定带宽。同时做几件事把流量压下来:静态资源走对象存储(不占服务器带宽,单价还便宜)、开启 gzip、给静态资源设置长缓存(Cache-Control: max-age=604800)、图片统一压缩到合适尺寸。这四项做完,同等访问量下的带宽消耗通常能降一半以上。
另外要留意出方向流量和入方向流量的计费差异,绝大多数场景是出方向收费、入方向免费,所以上传文件不心疼,下载和页面访问才心疼。
7.2 数据备份与故障恢复的最小方案
备份这件事,没出事的时候觉得多余,出了事就是救命。最小可用的方案是数据库每日全量导出 + 异地存储,脚本很简单:
#!/bin/bash # /opt/scripts/backup.sh set -euo pipefail DATE=$(date +%Y%m%d) BACKUP_DIR=/opt/backup mkdir -p "$BACKUP_DIR" # 数据库导出(按实际数据库类型调整) pg_dump -U appuser appdb | gzip > "$BACKUP_DIR/appdb-$DATE.sql.gz" # 只保留最近 7 天 find "$BACKUP_DIR" -name "appdb-*.sql.gz" -mtime +7 -delete # 上传到对象存储(用厂商提供的命令行工具) # ossutil cp "$BACKUP_DIR/appdb-$DATE.sql.gz" oss://my-backup/配到 crontab 里每天凌晨跑一次。关键在于必须验证恢复流程,我见过备份跑了半年,真要恢复的时候发现导出的文件是空的,因为数据库密码改了脚本一直失败但没人看日志。所以每隔一两个月,挑一份备份实际恢复到测试环境跑一次,确认能用。备份不做恢复验证,等于没备份。
配置文件的备份同样重要。/etc/nginx、/etc/systemd/system、.env这些改了之后没有版本记录,一旦机器挂了重建起来全靠记忆。我的做法是把这些配置也放进一个私有仓库,每次改动提交一次,注释写清楚改了什么、为什么改。
7.3 后续扩展的几个方向
如果这个服务后面要长起来,有几个扩展点值得提前想清楚。
应用多实例。当单进程扛不住的时候,最直接的办法是起多个进程监听不同端口,Nginx 里配upstream做负载均衡。这时候应用必须是无状态的,session 要么存 Redis,要么放加密 cookie,不能存在进程内存里。
静态资源分离。把图片、CSS、JS 全部迁到对象存储加 CDN,服务器只处理动态请求。这一步做完,服务器的带宽压力和 CPU 压力都会明显下降,是小站性价比最高的优化之一。
数据库独立。应用和数据库挤在一台机器上,初期没问题,但数据库的内存需求会随数据量增长,跟应用抢内存的情况迟早出现。数据量到一定规模之后,把数据库拆到独立实例上,应用机器只负责计算,扩容会简单很多。
日志集中化。机器多了之后,一台台登上去看日志不现实。可以在每台机器上装一个采集 agent,把日志汇总到一个地方统一检索。这个不用一开始就上,两三条机器以内还是直接 SSH 更快。
这套流程我从第一次摸索到现在,前后改过好几版,每版都是被坑出来的。最开始我也干过直接开应用端口、用 root 跑服务、日志不管不顾的事,后来每次出事都补一块,慢慢就成了现在这个样子。如果你刚开始做这件事,不必一次把上面所有内容都配齐,先把“进程守护 + 反向代理 + HTTPS + 安全组”这四件基础的事做对,服务就能稳定跑起来,剩下的加固和优化可以边跑边补。真正会让人手忙脚乱的,从来不是配置写得多复杂,而是某个半夜服务挂了,你却不知道从哪儿开始查。