
凌晨两点我盯着终端里滚动的日志又一次在翻一个月前自己写的部署记录。那种“我记得当时解决过但具体怎么做的来着”的窒息感让我下决心把所有散落在便签、网盘和个人博客草稿箱里的操作沉淀成一册。BHH的Trick小本本就这么来的。FBH不是某个高深框架我把这三个字母解释为“For Better Handling”一切为了让日常维护更好处理。这个本子不讲大而全的运维理论只收录每次真实踩坑之后验证过的命令行片段、容器配置、服务编排和备份流程。如果你和我一样自己用 Docker 跑着几个服务偶尔为 Nginx 配置挠头也会在半夜被失败的定时任务吵醒那这里面的内容应该能帮你少走几趟弯路。它们不是什么惊天动地的独门秘技更多是“早知道就好了”的操作习惯。真正的价值不在于某一条命令有多华丽而在于它出现得够及时能让你在事故现场直接用上。趁现在还记得清楚我把本子里的东西挑出几类最常用的按场景拆开写成一篇可以照着翻的实录。1. 这本小本本的由来与筛选标准1.1 为什么需要一册 Trick而不是收藏一堆文档刚开始折腾服务器的时候我也喜欢到处收藏教程浏览器里加了几十个书签网盘里躺着各种“史上最全命令清单”。可真到服务器出问题的时候这些收藏几乎派不上用场。原因很简单收藏夹里的东西是别人的我没有亲手验证过等到要用的时候还要重新判断它适不适合我当前的系统、版本和环境。踩了几次坑以后我换了一种做法。我把每个自己验证过的操作连同当时的报错信息、解决步骤和事后反思一起记下来形成了这本小本本。它不是一个知识库更像一个“操作现场实录”。比如“Nginx 返回 502”这种问题收藏夹里的答案是“检查后端服务是否启动”而我的小本本会写得更具体先看上游端口能不能通再看 Nginx 错误日志里有没有“connect() failed”最后才去看服务状态。这个顺序本身就是我从三次事故里换来的。这个小本本的适用范围很明确自托管玩家、用 NAS 和云主机跑私人服务的人以及刚接触运维但不想在基础问题上反复栽跟头的新人。它不是给资深运维专家看的因为专家不需要这种提醒它是给那些“有一定基础但经验还不够”的人准备的过渡工具。1.2 收录小本本的三条硬标准我给自己定过规矩不是什么内容都能塞进这个本子。有些东西看起来有用实际上只是占用空间。我筛选时只看三条标准解释反例可复制抄下来就能用不需要大量上下文解释“优化系统性能”这种纯概念已验证至少在自己的环境里成功执行过一次别人文章里转述但没试过的命令可重读三个月后翻出来依然能一眼看懂场景只记命令不记环境和报错的碎片这三条标准帮我过滤掉了大量冗余信息。比如“Docker 常用命令大全”这种内容虽然完整但它在需要时很难快速定位相反一条“清理悬空镜像但保留最近三个版本”的命令配上具体的执行参数和注意事项才是真正会被反复翻看的内容。每一条记录我都尽量按统一格式来场景描述、原始报错或现象、操作步骤、命令、验证方法、坑点备注。这样做的好处是哪怕当时记的时候觉得理所当然几个月后再看也不会云里雾里。1.3 我的笔记组织方式小本本不是只记“成功经验”更重要的是记“失败过程”。我习惯在每个解决方案旁边补一句“当初为什么走弯路”把错误判断也留下来。这种写法看起来不够简练但确实能帮助下一次遇到类似问题时更快进入状态。组织上我按主题分了几大类Docker、命令行、入口转发、证书、备份、定时任务。每类下面不按时间排而按“操作频次”排。最常用的放在最前面冷门的放后面。这个排列方式和教科书不一样但符合实际使用场景——翻小本本的人通常是在事故现场没空慢慢看目录。2. Docker 容器场景下的高频技巧2.1 临时想改容器配置别直接进容器动手我第一次改容器里的配置时用的是 docker exec -it 容器名 bash直接进去改文件。当时觉得挺顺畅直到容器重启所有修改全部消失我才意识到容器层是不可持久化的。那之后我学乖了改配置一律走宿主机。正确做法是先确认配置文件的位置然后把容器里的原始文件复制出来在宿主机上改。例如Nginx 容器的配置一般在 /etc/nginx/conf.d 下我先把整个配置目录拉出来做备份docker cp nginx:/etc/nginx/conf.d ./nginx-conf-backup改好之后再把文件复制回容器并且只重载不改结构docker cp ./default.conf nginx:/etc/nginx/conf.d/default.conf docker exec nginx nginx -t docker exec nginx nginx -s reload这里的关键点是先做语法检查再重载。我踩过一次坑改完配置直接 reload结果语法错误导致 Nginx 直接退出整个网站瘫了。后来我养成了“三步走”的习惯复制出来、语法检查、再重载。如果你没法在宿主机上操作那就手动把原始文件内容记录下来至少保证出问题时能改回去。还有一个更省心的办法启动容器时就把配置目录挂载出来。比如 docker run 或 compose 里挂载 -v /host/nginx:/etc/nginx/conf.d:ro这样以后改配置直接在宿主机上改容器里自动生效不需要复制来复制去。挂载方式更适合长期维护docker cp 则适合救急和一次性排查。2.2 清理 Docker 磁盘空间先看分布再动手Docker 用久了磁盘空间会莫名其妙变少尤其是经常拉镜像、重建容器的情况。我第一次清理时直接执行了 docker system prune -a结果把我还需要但暂时没运行的几个镜像全删了后来重新拉取浪费了不少时间。现在我清理之前会先执行docker system df这条命令会显示镜像、容器、卷、构建缓存的占用情况还能看到 RECLAIMABLE 列告诉你哪些空间可以回收。先看这个输出再决定清理策略。如果只是构建缓存多docker builder prune 就够了如果悬空镜像多docker image prune 能解决问题只有确定不需要保留的卷才能用 docker volume prune。这里特别提醒不要在服务器上随手执行 docker system prune -a --volumes。这个命令把所有未运行的容器、未使用的镜像和卷全部删除。卷里通常有数据一旦删了基本没法恢复。我在小本本里给自己定的原则是清理卷之前必须确认它对应的容器已经不存在并且不需要保留数据。如果需要更精准的清理我一般先列出悬空镜像确认镜像名和创建时间再手动删docker images --filter danglingtrue这样虽然步骤多一点但至少每一条清理动作都是自己确认过的不会出现“清理完才想起来某个镜像第二天要用”的尴尬局面。2.3 healthcheck 不是摆设它能决定服务编排顺序Docker Compose 启动多服务时最常见的坑是“数据库还没就绪应用已经启动然后连接失败”。依赖条件里的 depends_on 只能保证容器启动顺序不能保证服务内部真正就绪。解决办法是给服务加上健康检查。以 Web 应用依赖 Redis 为例我习惯在服务配置里这样写services: redis: image: redis:7 healthcheck: test: [CMD, redis-cli, ping] interval: 5s timeout: 3s retries: 10然后应用服务通过 depends_on 指定条件app: build: . depends_on: redis: condition: service_healthy这样一来应用容器会在 Redis 的 ping 返回 PONG 之后才启动基本能避免启动顺序问题。healthcheck 的间隔、超时和重试次数需要根据服务启动速度来调整。间隔太短会占用额外资源间隔太长则可能等很久才开始尝试重试次数太少则容易误判健康状态。有个坑需要注意很多精简镜像里没有 curl所以测试命令写得再优雅也没用。如果你不确定镜像里有没有 curl可以先执行 docker exec 容器名 which curl 看看没有就别硬写依赖 curl 的命令改用镜像自带工具或者用 wget、nc。我的做法是先在终端里手动执行一次健康检查命令确认返回结果符合预期再写进 compose 文件。3. 命令行与脚本里的救命细节3.1 用 alias 减少重复劳动但别覆盖系统指令日常操作中重复最多的命令我基本都做了别名。比如 docker compose、git status、进入上级目录之类都属于高频操作。在 .bashrc 或者 .zshrc 里加上几行别名能省下大量输入时间。alias dcdocker compose alias gsgit status alias gpgit push alias ..cd .. alias ..2cd ../.. alias cclear这里有一个我特别想强调的原则尽量不要覆盖系统已有的核心命令。比如把 rm 别名为“移动到回收站”听起来很安全但到了另一台没配置过的机器上你可能下意识认为自己执行了回收站逻辑结果直接删掉了文件。这种行为在企业生产环境里就是事故。我还吃过一个亏在某台机器上配了 alias llls -l后来写脚本的时候习惯性地用了 ll结果脚本在另一台机器上直接报 command not found。脚本是给机器读的不应该依赖用户级别名。所以在脚本内部永远写完整路径或完整命令需要短命令只在交互式终端里用。3.2 set -eu 是脚本的第一行但不能只靠它写 Shell 脚本的时候我坚持在开头加上 set -eu很多时候还要加 set -o pipefail。它们的作用是让脚本出错时立即停止而不是继续执行掩盖问题。#!/bin/bash set -eu set -o pipefail BACKUP_DIR/data/backup mkdir -p $BACKUP_DIR echo startset -e 的效果是命令返回非零值时立即退出。set -u 的效果是使用未定义变量时报警并退出。set -o pipefail 的效果是管道中只要有一个命令失败整个管道的返回就是失败。但这里有个容易让人栽跟头的例子。假设脚本里用 grep 判断一个字符串是否存在#!/bin/bash set -eu if echo hello | grep -q hi; then echo found else echo not found fi在 if 条件里使用 grep即使 grep 返回非零也不会触发 set -e 退出因为这是“被检查的条件”。这没问题。但如果你在脚本中间直接写 grep -q hi file文件里没有匹配项时grep 返回 1脚本就会立即退出。有时候这不是你要的效果所以需要显式加上 grep ... || true 来允许失败。我在实际维护中不止一次被 set -e“误杀”根源都是命令会因为“没找到匹配”而返回非零。理解了它的触发条件之后反而觉得它更可靠了。建议你写关键脚本时一定要在本地构造一个失败场景跑一遍看看脚本是不是在自己预期的地方停下而不是到了真正执行时才出问题。3.3 日志排错三板斧别对着大文件从头看到尾遇到服务异常最忌讳直接 cat 或 vim 打开一个几百 MB 的日志文件。不仅卡而且容易在无关信息里浪费时间。我的日志排错基本固定在三个步骤先看尾部、再过滤级别、最后做次数统计。tail -n 200 app.log第一板斧是拿最近的 200 行看看当前发生了什么。如果错误信息已经很明确直接解决问题如果不明确就用 grep 挑出关键词grep -E ERROR|Exception|Fatal app.log | tail -n 100第二板斧是把关注度集中在异常级别。注意顺序是先 grep 再 tail只取匹配结果里的最后 100 行因为错误信息可能非常多需要看最新的一批。第三板斧是统计错误出现的分布判断是偶发问题还是持续问题grep -E ERROR app.log | awk {print $1, $2} | sort | uniq -c | sort -rn这个命令会按日志里的前两列通常是日期和时间分组统计每个时间点出现的错误次数。通过这个结果能看出错误是不是集中爆发还是连续不断。做统计时关键是取舍字段。如果日志格式比较规整使用 awk 提取出你想要的时间单位即可如果不规整也可以把整行截断后再统计。这里的最终目的是快速定位影响范围而不是做精确的日志分析。4. 流量入口与网关转发4.1 别让服务裸奔在各个随机端口我早期自建服务的时候每个应用都直接监听一个端口然后记住“xxx 服务在 8080 端口、yyy 服务在 8081 端口”。这样做最直接的问题是记不住其次是不安全——所有端口都暴露在公网扫描范围内等于给攻击者提供了服务清单而且每新增一个应用都要新开一个端口。后来我把所有 HTTP 服务收进一个统一入口只在公网开放 80/443其余服务全部绑定到内网接口。Nginx 在这里的角色类似一个“门卫”通过不同的域名或路径把请求转发给对应的本地服务。核心思路是外部只能看到 Nginx具体服务藏在内网。基本配置思路如下server { listen 80; server_name app.example.com; location / { # 将请求转给本机的某个服务端口 set $backend http://127.0.0.1:8080; # 使用 rewrite 或内部跳转指令把请求交给上游 # 具体指令按自己使用的 Nginx 模块来定 } }上面这段我没有写具体的转发指令是因为不同版本的 Nginx 在模块选择上有差异。你在实际配置时需要根据自己安装的 Nginx 模块补齐这行内容。重点是那个思路对外只开一个入口对内走本地端口避免公网直接暴露应用。配置完之后记得用 nginx -t 检查语法再重载。我在一开始就吃过“只改配置不检查直接重载”的亏服务直接挂了。一定要把这两个步骤分开先检查再 reload。4.2 CORS 折腾记录Nginx 里的响应头怎么补前后端分离之后跨域问题就成了绕不开的坎。前端运行在某个域名后端接口在另一个服务上浏览器会拦截跨域请求需要通过后端返回 CORS 响应头来允许访问。我习惯在 Nginx 这一层统一处理 CORS而不是在每个应用里写一遍。这样后端服务可以不用关心跨域逻辑减少重复代码。基础配置可以这样写location /api/ { add_header Access-Control-Allow-Origin https://front.example.com always; add_header Access-Control-Allow-Methods GET, POST, OPTIONS always; add_header Access-Control-Allow-Headers Content-Type, Authorization always; }这里有个容易忽略的细节add_header 默认只在响应状态码为 200 的时候添加。但浏览器发起跨域预检请求时返回的是 204有些错误的跨域请求可能返回 4xx这时如果响应头没带过去前端一样会报错。解决办法是加上 always 参数强制在所有响应里加上指定头。这个细节我是在排查问题时发现的后来在笔记里用红笔标了出来。另外网上很多示例会配置if ($request_method OPTIONS) { return 204; }。这段逻辑是用来直接处理预检请求的。我第一次照抄时没理解为什么后来才知道预检请求本身不需要真正访问业务逻辑直接返回 204 能让前端更快完成跨域握手。如果你担心配置复杂也可以把这个判断逻辑放在应用层处理Nginx 只负责加响应头。4.3 证书自动续期重点是“重载”而不是“重启”自建服务启用加密访问之后证书过期成了新的常见事故。证书的有效期通常只有 90 天或更短手动更新很容易忘。我用 ACME 客户端来自动续期但写过定时任务之后才发现有些服务的证书更新后必须重载或重启进程才能生效。我的处理逻辑是这样的定时任务负责检查证书还有多久过期快过期时自动续期续期完成之后执行服务的 reload 命令。注意是 reload不是 restart。restart 会中断现有连接reload 则更平滑适合在线服务。以 Nginx 为例我在定时任务里加了这样一段#!/bin/bash # 续期证书并重载服务 /usr/bin/lego --email meexample.com --domains app.example.com --http renew --days 30 # 重载 Web 服务 systemctl reload nginx这里的关键是把“续期”和“重载”串在一起。如果只续期不重载文件虽然在磁盘上更新了但 Nginx 还在用旧证书访问时会频繁报证书错误。我踩过一次这样的坑证书明明续期成功但浏览器一直提示不安全最后才发现 Nginx 没重载。检查证书过期时间时我一般用 openssl 命令直接查看echo | openssl s_client -connect app.example.com:443 2/dev/null | openssl x509 -noout -enddate这个命令会从本地连接目标站点提取证书的过期时间。如果输出显示的时间已经接近当前时间说明自动续期或重载可能没生效需要立刻排查定时任务。5. 备份与任务自动化5.1 把 3-2-1 备份原则说成人话很多用户觉得自己有 NAS 就不用备份但实际上 RAID 不是备份它只防磁盘损坏不防误删、勒索软件和逻辑错误。我自己的备份习惯遵循 3-2-1 原则至少三份副本两种不同介质至少一份存放在异地。放在家庭服务器场景里可以这样落地副本一存在服务器本机磁盘副本二存到另一块物理硬盘或 NAS副本三存到云端或办公室的机器。本机备份用定时任务执行异地备份用同步工具完成。我常用 rsync 做增量同步基本命令是这样rsync -avz --delete /data/important /mnt/backup/参数 -a 表示归档模式-v 显示过程-z 压缩传输--delete 表示把远端多余的文件删掉使两端保持一致。要注意 --delete 是个双刃剑如果源目录路径写错了它会删掉备份里正常存在的文件。所以第一次执行时我会先加 --dry-run 跑一遍确认要删除的文件都在预期范围内再正式执行。数据库备份和文件备份要分开处理。文件可以 rsync数据库需要导出成固定格式再备份。以 PostgreSQL 为例pg_dump -U postgres mydb /data/dumps/mydb_$(date %F).sql备份文件要按日期命名保留最近 N 份避免旧文件无限堆积。我一般保留最近 14 天的日备份和最近 4 周的周备份用简单脚本自动清理。5.2 定时任务失败时要有“喊救命”的机制很多定时任务不是没执行而是执行失败了却没被发现。比如数据库备份cron 里写的是输出重定向到日志文件失败时也就是日志里多了一条记录不主动看根本不知道。我的做法是给关键任务设计一个“失败提醒”机制脚本失败时主动通知。最简单的方案是调用自建的提醒接口比如用 curl 触发一个 webhook然后再把日志尾部发过来。#!/bin/bash set -eu log_file/var/log/backup.log send_alert() { tail -n 20 $log_file | curl -sS -X POST https://notify.example.com/alert --data-binary - } trap send_alert ERR # 开始备份 rsync -avz /data/important /mnt/backup/ $log_file 21trap 的基本作用是在脚本收到指定信号时执行一段命令。ERR 表示脚本中某个命令返回非零时触发。这样一来rsync 失败时会自动调用 send_alert 函数把日志发给提醒接口。我试过很多种通知方式webhook 比邮件更及时也比短信更省钱。cron 里执行脚本时还有另一个坑环境变量不完整。交互终端里常用的 PATH 值cron 环境里可能没有。所以我的 cron 命令一律写绝对路径需要环境变量时在脚本里自行定义。如果发现定时任务“明明手动执行能成功cron 一跑就失败”八成就是环境变量的问题。5.3 配置入 Git灾难恢复不再从零开始以前我备份数据但忽略配置文件。容器编排、Nginx 配置、定时任务脚本这些都散落在系统各个角落一旦系统盘损坏恢复起来比想象中麻烦很多。后来我把所有文本配置集中到一个目录用 git 管理。这个目录可以这样组织server-configs/ ├── docker-compose/ │ ├── app1/ │ ├── app2/ ├── nginx/ │ ├── conf.d/ ├── scripts/ │ ├── backup.sh │ └── notify.sh └── .env.example在 .gitignore 里把真实密钥文件排除掉只提交模板和示例。例如把真实的 .env 忽略提交一个 .env.example里面用占位符代替真实值。这样既能保存完整的配置结构又不会把密码写进仓库。把配置纳入版本管理有一个额外的好处改动有历史记录。某次调整导致服务异常时可以直接 git diff 看出改了什么实在有问题就 git revert 恢复。我在一次系统盘损坏后的恢复过程中依靠这套配置仓库把服务重新拉起来省下了至少三个小时的排查时间。这个习惯非常值得形成。6. 常见问题排查速查表6.1 故障清单对照表下面这张表是我小本本里最常被翻的内容按症状排好附上第一步排查动作和常见原因症状第一步排查常见原因解决方向端口冲突docker ps | grep 端口容器启动顺序重叠释放端口或更换映射容器启动即退出docker logs 容器名健康检查失败或环境变量缺失检查启动命令和配置挂载定时任务不执行crontab -l 对比系统时间cron 服务未启动或路径不对启用 cron 服务并写绝对路径备份文件越来越大du -sh 备份目录未按日期清理增加清理保留 N 份的策略证书访问报错openssl s_client 查证书续期后未重载服务执行服务 reload网站响应极慢top 和 df -h内存不足或日志占满磁盘清理日志或扩容这张表只适合做快速定位不能替代完整排查。但大多数情况下快速定位已经能解决掉 80% 的问题。定位到原因之后再翻回前面具体章节的执行步骤比每次从零开始敲命令高效得多。6.2 快速定位的思路是按层切排查问题时我习惯按“客户端 → 入口 → 应用 → 日志 → 资源”这个顺序一层层切。先确认问题发生在哪一层。比如网页打不开时先看本机访问是否正常如果只外部无法访问问题大概率在入口规则或防火墙如果外部和本机都无法访问就要去查应用本身。日志是定位问题最直接的依据。不管是容器日志还是系统日志优先看最近一次错误发生前后的记录。不要一上来就怀疑核心组件先检查 IP、端口、路径这些基础信息是否匹配。很多“诡异问题”最后都发现是因为配置里写错了端口号。6.3 把失败记录也写进笔记记录故障时我除了记“怎么修的”还会写“当时的判断为什么错”。有一次数据库备份持续失败我一开始认为是磁盘空间不足检查半天发现是备份目录被挂载成了只读。这个错误判断本身也是一个有价值的记录。下次再遇到类似现象时我会先检查挂载状态而不是再次陷入磁盘空间排查。在笔记里记录失败的格式可以简化为三栏现象是什么、我当时怀疑什么、真正的根因是什么。不需要长篇大论三行足够。翻看旧记录时这些失败记录比成功记录更容易让人记住也更能在关键时刻提醒自己别走弯路。最后分享一点个人习惯。这本小本本我每个月会翻一次不是从头看而是按目录扫一遍标题顺手把过时的命令更新掉。内容少的时候翻完只需要十几分钟内容多的时候翻完还能顺带发现几个可以优化的地方。坚持下来小本本就成了一个活文档而不是死收藏。如果你也想建一个属于自己的 Trick 小本本我建议不要等到踩坑才开始写先把已经用过几次、还觉得好用的操作记下来等事故发生时它也许就是最可靠的那根拐杖。