服务器上最危险的东西,往往不是硬盘坏了,而是某个配置文件被人改过、却没人知道改了什么、什么时候改的。Nginx 突然 502、PHP 连不上数据库、SSH 再也登不进去——追查到最后,十有八九是 /etc 下的某个文件被改动过。本文讲一个几乎零成本、却能救命的习惯:用 etckeeper 把 /etc 整个目录纳入 Git 版本管理。
为什么 /etc 需要版本管理
绝大多数站长对代码会做版本控制,对 /etc 却完全放任。原因很简单:/etc 是"系统状态",不是"项目代码"。但现实是,/etc 里的东西和代码一样会被反复修改:装个软件、改个 Nginx 参数、调一下 PHP 的 php.ini、动一次防火墙规则。时间一长,没人记得住三天前把什么改坏了。
传统备份解决不了这个问题。备份是"某个时间点的快照",而你需要的是"两个时间点之间的差异"——是"谁在什么时候把这一行从 A 改成了 B"。这正是 Git 擅长的事。
etckeeper 就是干这个的:它是一组脚本,把 /etc 变成一个 Git(或 Mercurial、Bazaar)仓库,并且跟包管理器(apt/dnf/pacman)集成,在每次安装、升级、卸载软件包前后自动提交一次。这样你的 /etc 就有了完整的变更历史。
etckeeper 相比手动 git init 的优势
有人会说:我自己 cd /etc && git init 不行吗?可以,但会踩一堆坑,etckeeper 帮你解决了:
- 文件权限与属主:
/etc里有大量只有 root 能读的文件(比如shadow、私钥、sudoers),还有各种特殊权限位(setuid)。Git 默认不记录这些。.git目录如果权限设置不当,等于把服务器密码本公开。etckeeper 用etckeeper的元数据机制处理权限,并强制.git目录 700。 - 忽略规则:
/etc里有很多不该进版本库的东西,比如临时文件、缓存、mtab、resolv.conf这种由系统动态生成的、机器相关的文件。etckeeper 自带一套成熟的.gitignore。 - 与包管理器集成:手动 git 不会在 apt 升级后自动提交,你会漏掉一大堆变更。etckeeper 挂进 apt 的 hook,装包前后各提交一次,还带包名注释。
- 变更前后自动快照:配合
etckeeper vcs子命令,可以在任何操作前后由脚本触发提交。
安装与初始化
Debian/Ubuntu 上直接装:
sudo apt update sudo apt install etckeeper
安装过程本身会触发一次初始化。如果没触发,或者你想手动控制,可以运行:
sudo etckeeper init
这一步会在 /etc 下创建 Git 仓库,写入 .gitignore,并提交第一版(通常注释是 "Initial commit" 或 "saved /etc")。
安装时会有交互提示,重要的一项是是否允许 etckeeper 在 apt 操作时自动运行。默认是"是",建议保留。这一步实际上是往 /etc/apt/apt.conf.d/ 里放了一个 05etckeeper 钩子文件。
核心配置:/etc/etckeeper/etckeeper.conf
etckeeper 的主配置文件是 /etc/etckeeper/etckeeper.conf。几个关键选项:
# 使用哪个版本控制系统,取消注释对应行 VCS="git" #VCS="hg" #VCS="bzr" # 高层的元数据:记录权限、属主、空目录等 # 对 /etc 强烈建议开启 HIGHLEVEL_PACKAGE_MANAGER=1 # 使用 Git 时的提交选项 GIT_COMMIT_OPTIONS="" # 是否在 dailysnapshot 中自动提交 AVOID_DAILY_AUTOCOMMITS=1
大多数情况下默认配置就能用。HIGHLEVEL_PACKAGE_MANAGER=1 是推荐值,它会调用高级包管理钩子,记录得更完整。
如果你用的是 Git,还想让 etckeeper 在每次提交前做一些自定义检查,可以编辑 /etc/etckeeper/pre-commit.d/ 下的钩子脚本。
日常用法:查看、对比、回滚
etckeeper 本质上就是 Git 的包装,你可以直接进 /etc 用 git 命令,也可以用 etckeeper 提供的便捷子命令。
查看历史变更:
cd /etc sudo git log --oneline -20
你会看到类似这样的记录,装包产生的提交会自动带上包名:
a1b2c3d saving uncommitted changes in /etc prior to apt run
9f8e7d6 committing changes in /etc after apt run
Package: nginx看某个文件的历史:
sudo git log --oneline -- nginx/nginx.conf sudo git diff HEAD~5 -- nginx/nginx.conf
回滚单个文件到某个版本(这是最有用的操作之一):
# 查看 nginx.conf 的历史版本 sudo git log --oneline -- nginx/nginx.conf # 把它恢复到指定 commit 的状态 sudo git checkout <commit-hash> -- nginx/nginx.conf # 或者只撤销到上一个提交 sudo git checkout HEAD -- nginx/nginx.conf
回滚后记得重载对应服务,比如 sudo nginx -t && sudo systemctl reload nginx。
手动触发一次提交(改完配置后):
sudo etckeeper commit "调整 php.ini 上传大小限制"
查看整体状态:
sudo etckeeper vcs status
用 etckeeper 做"配置审计"
版本化之后,一项额外收益是"配置审计"。很多站长担心服务器被入侵后配置被偷偷篡改,etckeeper 正好能发现这个。你可以写个简单脚本,每天检查 /etc 是否有未提交的变更:
#!/bin/bash
# /root/check_etc.sh
cd /etc
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "警告:/etc 存在未提交的变更"
git status --short
fi把这段挂进 crontab,每天跑一次,把输出发给自己的邮箱或告警通道。正常情况下 /etc 不该有未提交的变更——如果有,要么是你自己刚改没提交,要么就是别人动了。
再进一步,可以用 git log --since="24 hours ago" --stat 列出最近一天所有改动过的配置文件和推送,形成一份"每日配置变更清单"。
异地备份:把仓库推到远程
etckeeper 的仓库默认只在本地。如果整台机器丢了,历史也就没了。建议加一个远程仓库:
cd /etc sudo git remote add origin git@your-git-server:/srv/git/etc-$(hostname).git sudo etckeeper commit "添加远程仓库" sudo git push -u origin master
注意:/etc 里包含 shadow、ssh_host_*_key 等敏感内容,远程仓库必须是私有且可信的。如果你介意,可以在 .gitignore 里排除这些文件——但那样就无法完整恢复,需要权衡。我的做法是自建一台内网 Git 服务器(Gitea),只在内网可达,物理上不暴露公网。
实战场景:一次 Nginx 配置灾难的复盘
讲一个很多站长都遇到过的真实场景。某天下午,你在 Nginx 里加了段 try_files 规则想优化伪静态,改完 nginx -t 通过,于是 systemctl reload nginx。上线后一切正常,就去忙别的了。三天后有用户反馈"文章页全变成 404 了",你打开浏览器一看,果然。这时你试图回想:三天前到底改了什么?是不是那次改 try_files 改错了?还是后来动了别的地方?
如果你有 etckeeper,这个过程会变得非常轻松。首先查看最近几天 /etc 的所有变更:
cd /etc sudo git log --since="7 days ago" --oneline sudo git log --since="7 days ago" --stat
--stat 会列出每次提交改动了哪些文件。nginx/sites-available/default 出现了好几次,说明它是"重点嫌疑"。接着看这个文件的完整变更历史:
sudo git log -p -- nginx/sites-available/default
-p 会直接打印每次提交的完整 diff。你会清晰地看到三天前那次改动把 try_files $uri $uri/ =404; 改成了 try_files $uri $uri/ /index.php?$args; 之类,而真正导致 404 的是更早某次把 location / 块挪了位置。所有疑问在一屏 diff 里全部解答。
确定问题后,回滚:
sudo git checkout <正确的commit> -- nginx/sites-available/default sudo nginx -t && sudo systemctl reload nginx
前后不到两分钟。没有 etckeeper 的话,你可能要在各种博客里翻找"我当初是怎么配的",或者干脆凭记忆重写一遍,结果引入新问题。这就是版本管理的价值:把"回忆"变成"查询"。
更妙的是,etckeeper 还能帮你回答"到底是谁改的"。如果是多人运维,每次提交前设置好 git config user.name,历史里就能看到操作者。再配合 SSH 的 auth.log(谁在什么时间登录),就能拼出完整的操作溯源链。
让编辑自动留痕:钩住编辑器与 shell
手动 commit 最大的问题是"容易忘"。有两个办法让留痕自动化。第一个是给 Vim 加一个退出时的检查钩子,或者更简单:在 ~/.bashrc 里为 root 定义一个包装函数:
# root 专属,放进 /root/.bashrc
nano() {
command nano "$@"
case "$1" in
/etc/*) (cd /etc && sudo etckeeper commit "edit $1") ;;
esac
}这样每次用 nano /etc/... 保存退出后,/etc 会自动提交一次,注释里带上被编辑的文件名。虽然粗糙,但能覆盖绝大多数手动改配置的场景。
第二个办法更彻底:很多运维团队会禁用直接编辑 /etc,所有变更走 Ansible/Puppet 这类配置管理工具。但那是另一个话题了——对小规模站长来说,"编辑器钩子 + 每日审计"已经足够。
五个必踩的坑
- 忘了提交,直接重启服务导致改动丢失。etckeeper 不会自动捕获你手动的改动,要靠
etckeeper commit或包管理器 hook。养成"改完配置立刻 commit"的习惯。更狠一点:给/etc下的编辑动作挂个 editor 退出钩子。 - .git 目录权限过大。如果
/etc/.git权限是 755,普通用户可能读到历史里的密钥。检查ls -ld /etc/.git,应为drwx------。etckeeper 默认会设置,但别手动破坏它。 - 把机器相关的动态文件提交进去。
resolv.conf、mtab、adjtime这些每台机器不同、还会自动变,提交进去会造成大量无意义 diff。etckeeper 的默认.gitignore已经处理了,但如果你用自定义的,要留意。 - 在容器里用 etckeeper。容器里
/etc通常是镜像的一部分,用 etckeeper 意义不大,而且会平白增大镜像。etckeeper 适合物理机或长期运行的 VPS。 - Git 提交时锁冲突。如果你的监控或脚本频繁触发提交,可能撞上
index.lock。避免在高频任务里调用etckeeper commit,或用flock串行化。
和备份的关系:不是替代品
最后强调一点:etckeeper 是"审计和回滚工具",不是备份工具。它记录文本差异,不能恢复被删除的二进制文件、数据库数据、网站内容。正确的组合是:
- etckeeper 管配置的历史与审计;
- rsync / borg / restic 管整体文件级备份;
- mysqldump / mariabackup 管数据库;
- 三者都定期同步到异地。
用 etckeeper 的成本极低——一条 apt install,之后几乎无感,但它能在"改坏了配置却想不起来改了什么"的时候,几秒钟就把你救回来。对每个认真做站的人来说,这都是一项值得立刻补上的基础设施。