ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Ubuntu下Apache2+HTTPS+s-nail+crontab运维闭环

Ubuntu下Apache2+HTTPS+s-nail+crontab运维闭环 上周帮人收拾一台跑了快三年的 Ubuntu 服务器需求听起来很朴素把站点从 http 换成 https服务器出问题的时候能自动发邮件通知顺便把几个定时任务从手动敲改成到点自己跑。结果一通折腾下来真正花时间的根本不是 apache2 的安装而是证书续期之后忘了重载、s-nail 的授权码写错导致静默退信、crontab 里脚本因为找不到 PATH 里的某个命令而每天准时失败一次。这套组合在 Ubuntu 上算是老配方了apache2 提供 Web 服务https 保证传输层加密s-nail 负责把服务器的求救信号送出来crontab 负责把所有周期性动作自动化。看起来四个不相干的东西实际上是一条完整的运维闭环。下面我把这套配置从思路到落地完整拆一遍包括每个参数为什么这么填、哪些坑我亲自踩过。不管你是第一次接触 Ubuntu 服务器配置还是已经用过一段时间但总在细节上翻车应该都能从里面捞到点能直接抄的东西。1. 这套组合到底解决什么问题整体思路与选型拆解1.1 从一台裸奔的 Ubuntu 说起很多人的 Ubuntu 服务器一开始都是最小化安装装个 apache2把网站丢进 /var/www/html浏览器能打开就算完事。这个时候的流量走的是明文登录表单、会话 Cookie、后台接口参数全都在链路上裸奔。再加上服务器没有任何主动通知机制磁盘写满了你不知道证书过期了浏览器报错用户跑了你也不知道备份脚本挂了三个月你更不知道。真正让人崩溃的不是配置本身有多难而是出事了没人告诉你。所以这套配置的真实目标是三件事。第一把 Web 流量从明文切到 https让传输内容有加密和完整性校验同时通过 301 跳转保证不会有用户停留在旧地址上。第二给服务器装一张嘴也就是 s-nail让它能在关键事件发生时通过外部 SMTP 把邮件发到我的邮箱里。第三给服务器装一套闹钟也就是 crontab让证书检查、日志清理、备份、状态巡检这些动作按固定节奏自动执行不用人记。这三件事单独做都不难难的是让它们互相咬合crontab 触发的脚本要用 s-nail 发信脚本要检查 https 证书剩余天数证书续期之后要让 apache2 重新加载。任何一环配错整条链路就是哑的。1.2 Apache2、HTTPS、s-nail、crontab 怎么串成一条线我习惯先把数据流向画清楚再动手不然配置写到一半就会晕。第一个环节是 apache2。它是整个链路里唯一对外暴露的服务负责监听 80 和 443处理虚拟主机、证书加载、跳转规则和响应头。它的健康状态直接决定用户能不能访问。第二个环节是 https。严格来说它不是一个独立组件而是 apache2 加载的一组模块ssl、headers、rewrite加上一对证书文件。证书来源可以是公网证书颁发机构也可以是自签前者适合有域名的正式站点后者适合内网和测试。第三个环节是 s-nail。它扮演的是发信客户端把 apache2 的错误日志摘要、证书剩余天数、磁盘占用情况这些信息通过外部邮件服务商的 SMTP 端口投递出去。注意这里它不负责收信也不建议在本机自建邮件服务原因后面细说。第四个环节是 crontab。它是调度器按时间表达式唤醒脚本。脚本里调用 openssl 检查证书、调用 df 检查磁盘、调用 s-nail 发信、调用 systemctl reload apache2 重载配置。串起来就是这样一条链路crontab 定时唤醒 → 脚本执行检查 → 发现异常 → s-nail 发信到我的邮箱 → 我收到通知去处理。证书续期这条支线则是crontab 定时唤醒 → certbot 续期 → 续期成功后 apache2 重载。把这两条线写进同一个脚本或者拆成两个任务都可以我倾向于拆开因为续期任务失败和巡检任务失败的排查方向完全不同。1.3 选型取舍为什么不用 Nginx为什么不用 msmtp有人肯定会问现在 Nginx 这么流行为什么还折腾 apache2。我的理由很实在apache2 的模块化配置对新手更友好a2enmod、a2ensite、a2dissite这三个命令把模块和站点的启用停用做成了开关不用手工去注释配置文件.htaccess支持让目录级配置可以不重启就生效遇到问题的时候apache2 的错误日志写得相当直白AH开头的错误码配上官方文档基本能自己定位。Nginx 在静态资源和高并发场景下确实更省资源但如果你的站点是中小流量、跑着 WordPress 或者一些老 PHP 应用apache2 的兼容性和调试体验反而更省心。这不是谁强谁弱的问题是场景匹配的问题。再说邮件客户端。Ubuntu 上可选的命令行发信工具有 s-nail、msmtp、mutt、sendmailpostfix 提供。sendmail 一整套邮件服务器太重本机起 SMTP 服务还容易变成垃圾邮件中继的跳板直接排除。mutt 更偏向交互式读信做纯发信有点杀鸡用牛刀。msmtp 很轻很好用但它是独立配置、独立命令很多老脚本里默认写的还是mail -s subject userexample.com。s-nail 恰好提供了mail命令的兼容入口在 Ubuntu 的 alternatives 机制里/usr/bin/mail通常就指向 s-nail。这意味着我写的脚本可以在不同机器上通用不用担心某台机器装的是 bsd-mailx 还是 msmtp 从而实现不一致。这是选 s-nail 最核心的理由兼容性和零改造成本。提示装完 s-nail 之后可以用readlink -f /usr/bin/mail确认 mail 命令的真实指向如果不是 s-nail用update-alternatives --config mail手动切换。2. 基础环境准备与 Apache2 落地2.1 系统状态确认与必装依赖动手之前先把系统状态摸一遍这一步很多人跳过后面出问题又回头补反而更慢。我一般按下面几条查lsb_release -a看发行版版本Ubuntu 20.04、22.04、24.04 在 apache2 默认配置和 Lets Encrypt 客户端上有细微差异。uname -r看内核版本顺便确认是物理机、虚拟机还是容器环境。ss -lntp看当前端口占用重点确认 80、443、25、587、465 有没有被别的东西占着。df -h看磁盘余量/var/log和/var/lib所在分区至少要留几个 G后面证书日志和 crontab 日志会慢慢长大。timedatectl确认时区和时间同步正常这个直接决定 crontab 什么时候执行、证书校验会不会因为时间偏差报错。依赖方面一次装齐比较省事sudo apt update sudo apt install -y apache2 openssl s-nail cron rsyslog acl这里每一项都有理由。openssl 用来做自签证书和证书信息读取s-nail 负责发信cron 是定时任务服务本体Ubuntu 最小化安装不一定自带rsyslog 保证 cron 日志能落盘acl 在某些部署脚本里会用到文件权限细粒度控制。装完之后养成一个习惯先看服务状态再看配置systemctl status apache2 --no-pager systemctl status cron --no-pager注意Ubuntu 上 cron 服务如果没装或者没启动你敲 crontab -e 是可以编辑的但任务永远不会跑。这个能编辑但不执行的状态坑过不少人。2.2 Apache2 目录结构与常用运维命令速查apache2 在 Ubuntu 上的目录布局是有约定的记住这套约定配置文件基本不用猜位置。路径作用备注/etc/apache2/apache2.conf主配置一般不改改动风险最高/etc/apache2/ports.conf端口监听80 和 443 的 Listen 指令在这里/etc/apache2/sites-available/可用站点配置写了不一定生效/etc/apache2/sites-enabled/已启用站点通常是软链接指向 available/etc/apache2/mods-available/可用模块模块的加载/卸载开关/var/log/apache2/日志目录access.log 与 error.log/var/www/站点根目录默认站点在 /var/www/html常用命令我整理成一个清单配置过程中会反复用到sudo apache2ctl configtest # 语法检查改完配置必须先跑这个 sudo systemctl reload apache2 # 平滑重载不中断现有连接 sudo systemctl restart apache2 # 完整重启改端口或核心模块时用 sudo a2enmod ssl # 启用 SSL 模块 sudo a2enmod rewrite # 启用重写模块做跳转用 sudo a2enmod headers # 启用响应头模块加 HSTS 用 sudo a2ensite example.conf # 启用某站点 sudo a2dissite 000-default.conf # 停用默认站点 apache2ctl -M # 列出已加载模块这里有个值得说的细节reload和restart的区别不只是中不中断这么简单。reload 是给主进程发信号让它重新读配置并重启子进程父进程保持存活已经建立的连接会优雅结束restart 则把主进程也干掉重来。涉及监听端口变更比如新增 Listen 443或者加载新模块的时候我建议直接用 restartreload 有时候会因为模块加载顺序问题出现配置看起来生效了但功能不正常的诡异状态。2.3 虚拟主机配置怎么写才不容易踩坑站点配置文件不要直接改 000-default.conf复制一份改名是最稳的做法sudo cp /etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/example.com.conf sudo a2dissite 000-default.conf虚拟主机的最小可运行骨架大概长这样VirtualHost *:80 ServerName example.com ServerAlias www.example.com ServerAdmin adminexample.com DocumentRoot /var/www/example.com/public Directory /var/www/example.com/public Options -Indexes FollowSymLinks AllowOverride All Require all granted /Directory ErrorLog ${APACHE_LOG_DIR}/example.com-error.log CustomLog ${APACHE_LOG_DIR}/example.com-access.log combined /VirtualHost逐行说一下为什么这么写。ServerName和ServerAlias决定请求命中哪个虚拟主机Apache 是按这个匹配的写错了就会出现访问域名打开了另一个站点的经典事故。Options -Indexes关掉目录列表防止别人直接访问一个没有 index 文件的目录时看到所有文件名。AllowOverride All允许目录级的 .htaccess 生效WordPress 之类的程序依赖它做永久链接如果你的站点不需要 .htaccess改成None性能会稍好一点点。ErrorLog和CustomLog我特意加了站点名前缀而不是共用默认日志这样多站点环境下排查问题时不会互相干扰。提示DocumentRoot 指向的目录如果不存在apache2 不会报错只会返回 403 或者 404。配置完之后一定确认目录真实存在ls -ld /var/www/example.com/public。3. HTTPS 配置全流程从证书申请到强制跳转3.1 公网证书与自签证书的取舍这一步的选择完全取决于你的场景没有什么最佳实践可以一刀切。有公网域名、站点对外的用 Lets Encrypt 免费证书理由很直接浏览器信任、90 天有效期配合自动续期可以长期免维护、申请过程全自动。Ubuntu 上现在推荐用 snap 安装 certbotsudo snap install core sudo snap refresh core sudo snap install --classic certbot sudo ln -s /snap/bin/certbot /usr/bin/certbot然后一条命令搞定申请和自动写入 apache 配置sudo certbot --apache -d example.com -d www.example.com这条命令会做三件事通过 HTTP-01 挑战验证域名归属、把证书写到 /etc/letsencrypt/live/example.com/、自动生成或修改 apache 的 SSL 虚拟主机配置。如果你已经在 sites-available 里手工写好了 SSL 配置可以用certonly子命令只申请证书不动配置sudo certbot certonly --apache -d example.com -d www.example.com没有公网域名、只是内网测试或者自用服务的用自签证书。生成命令如下sudo mkdir -p /etc/ssl/private /etc/ssl/certs sudo openssl req -x509 -nodes -days 825 -newkey rsa:2048 \ -keyout /etc/ssl/private/example.key \ -out /etc/ssl/certs/example.crt \ -subj /CCN/STZhejiang/LHangzhou/OHomeLab/CNexample.local几个参数值得解释。-nodes是no DES意思是私钥不加密这样 apache2 启动时不用手输密码否则重启服务会卡在那里等人交互。-days 825是有效期天数不要贪心地写 3650部分客户端对超长有效期的证书有额外限制825 天是浏览器生态里比较安全的范围。-newkey rsa:2048生成 2048 位 RSA 密钥兼容性最好如果你确定服务端和客户端都较新可以用ecdsa参数换椭圆曲线密钥握手更快。-subj一次性把主题信息填完避免交互式输入。CN 字段对于自签证书来说如果是现代浏览器访问 IP 地址建议把 IP 写进 SAN 扩展否则会报证书名称不匹配。要加 SAN 的话需要额外写配置文件用-addext参数sudo openssl req -x509 -nodes -days 825 -newkey rsa:2048 \ -keyout /etc/ssl/private/example.key \ -out /etc/ssl/certs/example.crt \ -addext subjectAltNameDNS:example.local,IP:192.168.1.10 \ -subj /CNexample.local注意自签证书的私钥文件权限一定要控制住chmod 600是底线/etc/ssl/private目录本身在 Debian 系里默认是 710别随便改成 755。3.2 Apache SSL 虚拟主机逐行解析启用 SSL 模块之后新建一个 443 的虚拟主机配置sudo a2enmod ssl sudo a2enmod headers sudo cp /etc/apache2/sites-available/example.com.conf /etc/apache2/sites-available/example.com-ssl.confSSL 配置的核心内容IfModule mod_ssl.c VirtualHost *:443 ServerName example.com ServerAlias www.example.com DocumentRoot /var/www/example.com/public SSLEngine on SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384 SSLHonorCipherOrder off SSLSessionTickets off Header always set Strict-Transport-Security max-age31536000 Directory /var/www/example.com/public Options -Indexes FollowSymLinks AllowOverride All Require all granted /Directory ErrorLog ${APACHE_LOG_DIR}/example.com-ssl-error.log CustomLog ${APACHE_LOG_DIR}/example.com-ssl-access.log combined /VirtualHost /IfModuleSSLCertificateFile用 fullchain.pem 而不是 cert.pem区别在于 fullchain 包含中间证书客户端能顺着链条验证到根证书用了 cert.pem 的话部分安卓设备和老版本客户端会出现证书链不完整的报错。SSLProtocol里显式排除了 SSLv3 和 TLS 1.0/1.1这几个协议版本已经被主流浏览器标记为不安全留着只会让安全扫描报告飘红。SSLCipherSuite只保留 ECDHE 前缀的套件这类套件支持前向保密即使私钥未来泄露历史流量也无法被解密。SSLHonorCipherOrder off让客户端优先选择对移动端兼容性更好服务端强制的话有些老客户端会握手失败。SSLSessionTickets off关掉会话票据避免票据密钥无法轮换带来的隐患代价是复用率降低、握手稍多对中小站点没影响。改完配置一定要跑语法检查sudo apache2ctl configtest看到Syntax OK再重载否则一个拼写错误就能让整个 Web 服务起不来。这里有个自救技巧如果重载之后服务挂了systemctl status apache2会告诉你具体哪一行有问题改回来再 reload 就行别慌。3.3 301 强制跳转与安全响应头http 的 80 端口不能直接关掉因为 Lets Encrypt 的续期验证、部分老用户的直接输入、一些外部服务的回源都可能走 80。正确做法是保留 80但把所有请求 301 到 https。两种写法第一种用 Redirect简单直接VirtualHost *:80 ServerName example.com ServerAlias www.example.com Redirect permanent / https://example.com/ /VirtualHost第二种用 mod_rewrite适合需要保留路径和参数的场景VirtualHost *:80 ServerName example.com ServerAlias www.example.com RewriteEngine On RewriteCond %{HTTPS} off RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R301,L] /VirtualHostRewriteRule里的$1把原始路径原样带到 https 地址上[R301,L]表示永久跳转并且这是最后一条规则。用%{HTTP_HOST}而不是硬编码域名好处是 www 和非 www 都能正确跳到自己对应的 https 地址不会出现 www 被吞掉的问题。HSTS 响应头我单独说因为它是把双刃剑。加上Strict-Transport-Security: max-age31536000之后浏览器在一年内会强制用 https 访问这个域名即使你手动输入 http 也会被浏览器内部改写。好处是防降级攻击和 Cookie 劫持坏处是如果哪天你的证书出问题、或者你想回退到 http 调试浏览器根本不给机会。所以我的做法是先用一个较短的时间比如max-age300跑几天确认没问题再逐步加长到一年。真要加includeSubDomains更要谨慎它会把所有子域名一起锁进 https包括你可能还没配证书的那些。提示HSTS 一旦下发清除方式是让浏览器访问 chrome://net-internals/#hsts 删掉对应域名的记录但普通用户做不到所以生产环境下宁可保守一点。3.4 证书自动续期与重载的坑Lets Encrypt 证书 90 天有效期靠 certbot 的 systemd timer 自动续期systemctl list-timers | grep certbot sudo certbot renew --dry-run--dry-run是必须做的一步它会用测试环境模拟一次续期验证你的验证路径、DNS 解析、80 端口回源是否正常。很多人是在证书真正过期前一周才发现续期失败因为错误的续期日志默认只会发到 root 邮箱而 root 邮箱又没配。真正的大坑是续期成功但服务没生效。certbot 的 renew 只会把新证书写到 /etc/letsencrypt/live/ 目录下并不会自动让 apache2 重新加载。apache2 在启动时已经把证书读进内存了文件更新了但进程不知道用户看到的还是旧证书。解决办法是加一个部署钩子在 /etc/letsencrypt/renewal-hooks/deploy/ 目录下放一个可执行脚本sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh /dev/null EOF #!/bin/bash systemctl reload apache2 EOF sudo chmod x /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh这个脚本只在续期真正发生证书确实更新了时才执行dry-run 不会触发它。文件必须可执行否则会被忽略且不报错——这是我最想强调的一个坑因为脚本没加执行权限的时候日志里什么提示都没有你会以为配好了直到某天发现证书过期。4. s-nail 打通服务器告警邮件通道4.1 安装与外部 SMTP 配置s-nail 的定位是邮件用户代理它只负责把信交给上游 SMTP 服务器不负责收信和存储。这个定位很重要因为它在设计上就不适合拿来搭邮件服务器。配置分全局和用户级两种全局的放在 /etc/s-nail.rc用户级的放在 ~/.s-nailrc优先级是用户级覆盖全局。全局配置示例sudo tee -a /etc/s-nail.rc /dev/null EOF set frommonitorexample.com set smtpsmtps://smtp.example.com:465 set smtp-auth-usermonitorexample.com set smtp-auth-password这里填授权码 set smtp-authlogin set ssl-verifyignore set nss-config-dir/etc/pki/nssdb set charsetUTF-8 EOF逐项解释。from必须和你用来认证的邮箱一致绝大多数邮件服务商都会校验发件人和认证账号的匹配关系不一致直接拒收。smtp后面的smtps://前缀表示用隐式 TLS对应 465 端口如果用 587 端口做 STARTTLS写法是smtp://smtp.example.com:587同时加上set smtp-use-starttls。smtp-auth-password这里千万不要填邮箱的登录密码主流的邮箱服务都需要在后台单独生成一个授权码或应用专用密码这是两套东西填错了会一直返回 535 认证失败。ssl-verifyignore是跳过证书校验方便但不够严谨如果你的服务器 CA 证书库是完整的建议改成ssl-verifystrict。nss-config-dir指向 NSS 证书库目录s-nail 在某些编译选项下依赖它做 TLS 校验不配的话可能报证书库相关的错。文件权限也要注意里面有授权码chmod 600 /etc/s-nail.rc是必须的否则同机器上的其他用户能直接读到你的邮箱凭据。4.2 发信测试与退信排查配置完立刻测试不要等到告警脚本跑起来才发现发不出去echo 这是一封来自 Ubuntu 服务器的测试邮件。 | s-nail -s 服务器邮件通道测试 your-mailexample.com如果这条命令卡住不动通常是网络不通或者端口被挡如果秒退但没收到信去看返回码。常见返回码和原因对照返回码含义常见原因535认证失败用了登录密码而非授权码认证方式与端口不匹配550发件人被拒from 与认证账号不一致发件域名被列入策略554内容被拒主题或正文触发了反垃圾规则连接超时端口不通465 被网络策略拦截换 587 试无任何输出但未送达静默失败脚本里把错误重定向丢弃了排查顺序我一般是这样的先用s-nail -d打开调试模式看完整握手过程加-v看更详细的交互再用telnet smtp.example.com 465或者openssl s_client -connect smtp.example.com:465确认端口可达然后检查 from、认证账号、授权码三者的对应关系。这个顺序的原因是认证类问题占比最高但端口不通的问题排在最前面排查成本最低。注意如果你的服务器出口 IP 是共享的或者被列入了某些黑名单即使配置全对也可能被收件方拒收。这种情况只能换发信通道或者联系收件方处理单纯调配置解决不了。4.3 把监控脚本接进来邮件通道通了之后就可以写真正的巡检脚本了。我的习惯是脚本开头做几件事设置 PATH、设置字符集、定义收件人、定义日志文件。#!/bin/bash export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin export LANGC.UTF-8 MAILTOyour-mailexample.com HOSTNAME_SHORT$(hostname -s) LOG/var/log/server-check.log log() { echo [$(date %Y-%m-%d %H:%M:%S)] $* $LOG } # 证书剩余天数检查 DOMAINexample.com END_DATE$(echo | openssl s_client -servername $DOMAIN -connect $DOMAIN:443 2/dev/null \ | openssl x509 -noout -enddate 2/dev/null | cut -d -f2) if [ -n $END_DATE ]; then END_TS$(date -d $END_DATE %s) NOW_TS$(date %s) DAYS_LEFT$(( (END_TS - NOW_TS) / 86400 )) log 证书剩余天数: $DAYS_LEFT if [ $DAYS_LEFT -lt 15 ]; then echo 服务器 $HOSTNAME_SHORT 上 $DOMAIN 的证书还有 $DAYS_LEFT 天到期请检查续期任务。 \ | s-nail -s [告警] $DOMAIN 证书即将到期 $MAILTO log 已发送证书到期告警 fi else log 无法读取证书信息 echo 服务器 $HOSTNAME_SHORT 无法读取 $DOMAIN 的证书信息请排查。 \ | s-nail -s [告警] 证书读取失败 $MAILTO fi # 磁盘使用率检查 DISK_USED$(df -P / | awk NR2 {print $5} | tr -d %) log 根分区使用率: ${DISK_USED}% if [ $DISK_USED -gt 85 ]; then echo 服务器 $HOSTNAME_SHORT 根分区使用率已达 ${DISK_USED}%。 \ | s-nail -s [告警] 磁盘空间不足 $MAILTO log 已发送磁盘告警 fi这里有几个细节值得说。第一所有命令都用绝对路径或者在开头统一设置 PATH因为 crontab 执行脚本时的环境变量和你在终端里完全不一样这个后面单独讲。第二2/dev/null把 openssl 的握手日志丢掉否则会污染变量。第三date -d把证书的日期字符串转成时间戳再算差值直接用字符串比较是不可靠的。第四每个关键动作都写日志这个日志在排查任务到底跑没跑的时候是救命稻草。5. crontab 定时任务编排实战5.1 时间表达式与最小可运行示例crontab 的时间表达式有五个字段顺序不能错字段含义取值范围特殊符号第 1 位分钟0-59* , - /第 2 位小时0-23* , - /第 3 位日1-31* , - /第 4 位月1-12* , - /第 5 位星期0-70 和 7 都是周日* , - /常用写法先记几个。0 8 * * *表示每天 8 点整执行一次注意是每天 8:00:00不是 8 点这一小时内随机。*/5 * * * *每 5 分钟一次。0 */2 * * *每两小时整点。0 3 * * 0每周日凌晨 3 点。0 2 1 * *每月 1 号凌晨 2 点。一个容易踩的认知误区日期字段和星期字段是或的关系不是与。如果你写0 0 1 * 1它会在每月 1 号执行也会在每个周一执行而不是每月 1 号且是周一才执行。想实现后者必须在脚本里自己判断。编辑用户的 crontab 用crontab -e第一次会问你选编辑器选 vim 还是 nano 看习惯。列表用crontab -l删除用crontab -r——这个命令要特别小心它会直接删掉当前用户的所有定时任务没有任何二次确认我见过不止一个人手滑把整台机器的任务清单清空。5.2 crontab: command not found 与 cron 服务自启很多人第一次在最小化安装的 Ubuntu 上敲 crontab 会碰到这个-bash: crontab: command not found这通常有三种可能。第一种是 cron 包没装apt install cron解决。第二种是装了但 /usr/bin/crontab 不在 PATH 里用which crontab和ls -l /usr/bin/crontab确认。第三种最隐蔽是你在容器里或者某个受限 shell 环境下PATH 被裁剪过。前两种好办第三种要检查echo $PATH。装上之后还有一个比命令不存在更让人头疼的状态sudo systemctl status cron如果显示 inactive那么你编辑的任务不会有任何执行。临时启动用sudo systemctl start cron设为开机自启用sudo systemctl enable cron。这两个我建议直接合并执行sudo systemctl enable --now cron装完服务记得再看一眼有没有被别的调度器干扰。有些镜像里会同时装 cronie 或者 systemd timer 版的定时任务两套东西并行跑同一份脚本会造成重复执行日志里会出现莫名其妙的两条记录。5.3 环境变量定时任务静默失败的元凶这是 crontab 使用中最经典的问题几乎没有之一。你在终端里手动跑脚本一切正常写进 crontab 里就啥也不发生日志里连报错都看不到。原因就在于 cron 执行任务时的环境和登录 shell 完全不同PATH 通常只有/usr/bin:/bin你在 ~/.bashrc 里加的 /usr/local/bin、/opt/xxx/bin 全都失效。HOME、USER、SHELL 这些变量可能缺失或不同。不会加载 ~/.bashrc、~/.profile、/etc/profile。LANG 可能是空或者 C中文字符会乱码甚至导致脚本解析异常。cron 默认的 shell 是 /bin/sh不是 /bin/bash一些 bash 特有的语法比如[[ ]]、数组、source会直接报错。应对方式有三层我一般三层都上稳一点。第一层在 crontab 文件顶部定义环境变量PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LANGC.UTF-8 SHELL/bin/bash MAILTOMAILTO这一行我特意加上作用是抑制 cron 把脚本的标准输出当作邮件发送。如果不设cron 会尝试用本机邮件系统给当前用户发信而本机通常没有可用的 MTA结果就是日志里堆一堆投递失败的记录把真正的错误淹掉。既然我们已经用 s-nail 主动发告警了就让 cron 自己的邮件机制闭嘴。第二层在 crontab 里显式指定解释器0 8 * * * /bin/bash /opt/scripts/daily-check.sh /var/log/daily-check.log 21第三层在脚本内部再做一次 PATH 和 LANG 的兜底设置也就是前一份脚本开头那两行 export。三层叠加看起来很啰嗦但这是我从无数次任务不执行里总结出来的最低成本方案——因为每次排查环境变量问题都要花半小时而写这三行只要十秒钟。提示想验证环境差异可以在 crontab 里加一条* * * * * env /tmp/cron-env.txt等一分钟看文件内容你就知道 cron 到底给了你什么环境。用完记得删掉这条测试任务。5.4 日志落地与执行追踪crontab 本身不保存任务输出除非你重定向。所以查看 crontab 执行日志这件事要分两层看。第一层是 cron 服务自己的日志记录它有没有去调用你的任务grep CRON /var/log/syslogUbuntu 上 cron 的日志默认混在 syslog 里。如果 grep 不到任何东西检查 /etc/rsyslog.d/50-default.conf 里是不是有这一行被注释掉了cron.* /var/log/cron.log取消注释之后sudo systemctl restart rsyslog之后 cron 的日志就单独写到 /var/log/cron.log 了排查起来清爽很多。注意这个改动会影响到所有写到 cron.log 的记录如果 /var/log 分区小记得配合 logrotate。第二层是任务脚本自己的输出必须在 crontab 行里显式重定向0 8 * * * /bin/bash /opt/scripts/daily-check.sh /var/log/daily-check.log 21是追加模式21把标准错误也合并进去。我强烈建议不要用覆盖模式因为一旦某次执行出问题你回看日志会发现之前的记录全被冲掉了问题现场直接丢失。日志文件要配合 logrotate 定期切割否则按天累积一年也是一笔不小的磁盘开销。再加一个实用技巧在脚本最后输出一行带时间戳的结束标记这样你翻日志的时候能一眼看出每次执行是正常结束还是中途挂掉log 任务结束退出码 0如果日志里只有开始没有结束那说明脚本在中途异常退出了直接往中间那段代码找问题。一个完整的、可直接参考的 crontab 配置示例PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin SHELL/bin/bash LANGC.UTF-8 MAILTO # 每天 8 点执行服务器巡检并发送告警邮件 0 8 * * * /bin/bash /opt/scripts/daily-check.sh /var/log/daily-check.log 21 # 每天 3 点尝试续期证书certbot 内部会判断是否需要续期频繁调用无害 30 3 * * * /usr/bin/certbot renew --quiet /var/log/certbot-renew.log 21 # 每周日凌晨 4 点清理 7 天前的日志 0 4 * * 0 /usr/bin/find /var/log -name *.log.* -mtime 7 -delete /var/log/log-clean.log 216. 常见问题与排查速查表6.1 HTTPS 与证书类现象可能原因排查动作浏览器报证书不受信任用了自签证书或证书链不完整检查是否使用 fullchain.pem自签证书导入受信任根访问 https 打不开但 http 正常443 未监听或防火墙未放行ss -lntp | grep 443apache2ctl -M | grep ssl续期成功但浏览器仍显示旧证书apache2 未重载检查 deploy hook 是否存在且有执行权限提示 TLS 版本过低客户端太老服务端已禁用旧协议临时放宽 SSLProtocol 验证确认后再决定是否保留部分路径跳转后 404跳转规则丢失了路径检查 RewriteRule 里有没有$1“Redirect permanent /” 是否写成完整路径这里我单独展开说证书链不完整这个现象因为它特别有迷惑性。你用 curl 在本机测是正常的因为本机信任 /etc/ssl/certs 里的自签根但用户用手机浏览器打开就报错。原因在于服务端只发了叶子证书没发中间证书。验证方法是openssl s_client -connect example.com:443 -servername example.com -showcerts输出里如果证书链只有一张那就是不完整。解决方式很简单换成 fullchain.pem 就行。这个问题在国内某些移动网络环境下尤其容易被放大因为部分终端的证书库更新不及时。6.2 邮件与定时任务类现象可能原因排查动作crontab 命令不存在cron 未安装apt install cron并systemctl enable --now cron任务到点不执行cron 服务未运行脚本无执行权限shebang 缺失依次检查这三项任务执行但脚本内命令找不到PATH 不完整crontab 顶部定义 PATH脚本内用绝对路径手动执行正常定时执行失败环境变量差异工作目录不同在脚本开头打印pwd、env、$PATH对比收到大量来自 cron 的空邮件MAILTO 未设置任务有标准输出crontab 里设MAILTO并重定向输出s-nail 返回 535密码用的是登录密码不是授权码去邮箱后台重新生成授权码邮件延迟很久才收到发信通道排队或触发风控换端口试减少发信频率检查收件方策略中文主题或正文乱码字符集未设置s-nail 配置加set charsetUTF-8脚本 LANG 设 C.UTF-8关于手动执行正常定时执行失败这一类问题我有个固定的排查套路你可以直接照抄。第一步在 crontab 里把任务改成每分钟执行一次同时把输出重定向到临时日志。第二步看日志有没有内容没有内容说明任务根本没被触发问题在 cron 服务或表达式有内容但报错问题就在脚本内部。第三步在脚本最开头插入三行诊断输出echo PWD$(pwd) /tmp/cron-debug.log echo PATH$PATH /tmp/cron-debug.log echo USER$(id -un) /tmp/cron-debug.log第四步用这些信息和你终端里的环境对比差异点往往就是答案。这个套路看起来笨但比盲目猜测快得多。6.3 个人避坑清单下面这些是我在实际操作中反复确认过的经验写在这里省得你一个个踩。不要在 crontab 里直接写复杂的多命令管道。crontab 只认一行一条任务复杂的逻辑全部封装进脚本文件crontab 只负责调它。写在 crontab 里的管道一旦涉及引号嵌套转义规则会让人抓狂而且改起来不方便。不要用 root 的 crontab 跑所有任务。权限过大意味着脚本里一个路径写错就可能删掉重要文件。用专门的运维用户跑需要重载服务的时候用 sudoers 精确授权某几条命令比裸奔安全得多。不要在脚本里硬编码密码和授权码。s-nail 的配置放 /etc/s-nail.rc 并设 600 权限是一种方式更规范的是放进单独凭据文件脚本运行时读取并且确保这个文件不在备份工具的同步范围内。不要忽略时区。你的服务器可能是 UTC你在东八区0 8 * * *的结果可能是你下午 4 点收到邮件。用timedatectl确认时区或者在写任务时按 UTC 换算两种都行但必须心里有数。不要忘了给部署钩子加执行权限。前面强调过一次这里再提一次因为它造成的故障是静默的。不要把所有告警发到同一个高频渠道。磁盘告警、证书告警、任务失败告警混在一起很快就没人看了。我一般把证书和磁盘这类需要尽快处理的分到一个邮箱把任务执行失败这类可以稍后看的分到另一个避免重要信息被淹没。7. 收尾几个我踩过之后的实际体会这套配置我前后在三台机器上做过最大的感受是真正让系统稳的不是配置写得多漂亮而是失败的时候你能不能知道。apache2 的配置、证书的申请、s-nail 的参数、crontab 的表达式这些看文档都能学会但把它们串起来之后出现的组合故障文档里基本不会写。比如证书续期成功但服务没重载日志里一片绿灯比如 crontab 任务因为 PATH 缺一个目录而每天准时失败syslog 里只有一行执行记录没有错误比如 s-nail 配置全对但出口 IP 被对方策略拦了返回码还是 535 让人误以为是密码问题。这些问题只能靠日志、靠主动通知、靠反复验证来暴露。所以我现在的习惯是任何新写的定时任务上线第一天一定先手动执行一遍再把时间改成每分钟一次观察半小时确认输出正常、邮件能到、日志完整才改回正常频率。证书这块除了 certbot 自带的续期我一定额外写一个独立的剩余天数检查因为它不依赖 certbot 的状态是从外部真实请求一次获取的证书信息两套机制互相印证。邮件这块配置完立刻发一封测试信之后每周让巡检脚本顺带发一封心跳邮件如果某周没收到心跳说明巡检脚本本身挂了——这是一个不需要额外监控系统的自我监控手段。如果你手上也有一台正在跑 apache2 的 Ubuntu建议按这个顺序推进先把 apache2 虚拟主机配置理顺再上 https 和跳转接着打通 s-nail 的测试邮件最后用 crontab 把所有检查动作固化下来。顺序不要颠倒因为后面的每一步都依赖前一步可验证。至于那些参数细节不用一次背下来需要的时候翻回来看对照表就行真正重要的是养成改配置前跑 configtest、上线前先手动验证、所有输出都落盘这三个动作习惯它们比任何一条具体配置都值钱。
返回列表