1. 从一次凌晨的容器启动失败说起
凌晨两点,测试环境的部署流水线突然卡住了。运维同事在群里甩了一张截图:Job for docker.service failed because the control process exited with error code。我第一反应是磁盘满了或者内核参数被改坏了,结果登上机器一看,systemctl status docker给出的报错信息里赫然出现了/etc/hosts相关的字样。那一刻我才意识到,问题出在自己白天为了调试某个内部服务,手动往宿主机/etc/hosts里加了几行域名映射,而这几行内容恰好踩中了 Docker 启动时解析配置的雷区。
这个场景其实非常典型。很多人在配置 Docker 的hosts时,脑子里想的是"给容器加个域名解析",但实际操作时却分不清三个完全不同的层面:宿主机的/etc/hosts、Docker 守护进程的启动参数、以及容器内部的--add-host。一旦把这三者混为一谈,轻则容器内解析不生效,重则直接导致docker.service起不来,整个节点上的容器全部失联。这篇文章就是围绕"docker 配置 hosts 无法启动 docker 服务"这个具体故障,把背后的机制、排查链路、修复方案和长期规避策略一次讲透。不管你是刚接触 Docker 的新手,还是已经管过几十个节点的老运维,只要你的工作里涉及容器网络和域名解析,这篇内容都值得花时间看完。
我先给一个结论性的判断:绝大多数"配置 hosts 导致 Docker 服务无法启动"的问题,根源都不在容器内部,而在宿主机层面或 Docker 守护进程的配置加载环节。容器内部的--add-host写错了,最多是那个容器起不来,不会把整个docker.service拖垮。真正能让 Docker 守护进程直接挂掉的,是宿主机/etc/hosts格式异常、daemon.json里塞了非法字段、或者 systemd 单元文件被错误覆盖。下面我按排查顺序,一层一层拆开讲。
2. 宿主机 /etc/hosts 写错为什么能拖垮整个 Docker 守护进程
2.1 Docker 启动时对 /etc/hosts 的依赖链路
很多人不知道,Docker 守护进程在启动阶段就会读取宿主机的/etc/hosts。这不是 Docker 多此一举,而是因为它需要解析自身配置里出现的各种主机名,比如daemon.json中配置的registry-mirrors、insecure-registries、dns等字段如果包含域名,守护进程在初始化网络模块时会尝试解析。更关键的是,Docker 在创建默认的bridge网络时,会读取宿主机的 DNS 和 hosts 配置来决定容器默认的解析行为。
如果/etc/hosts文件格式不合法,比如某一行只有 IP 没有主机名、主机名里包含了非法字符、或者文件编码变成了 UTF-8 BOM 开头,Docker 在解析时就会抛出异常并直接退出。这种退出不是"降级运行",而是 systemd 判定服务启动失败,触发start-limit保护,短时间内反复重启失败后彻底锁死。
我遇到过最隐蔽的一种情况:某同事用 Windows 编辑好 hosts 内容后通过共享目录传到 Linux 服务器,文件开头带了一个不可见的 BOM 字符。cat出来看完全正常,但 Docker 解析第一行时就报invalid host entry。这种问题用肉眼几乎发现不了,必须靠xxd或file命令才能定位。
2.2 一个真实故障的完整排查链路
回到开头那次凌晨故障,我把排查过程完整还原一下,你可以对照自己的场景复现:
- 确认服务状态:
systemctl status docker -l,看到Active: failed,并且日志里有一行unable to parse /etc/hosts: invalid argument。 - 查看详细日志:
journalctl -u docker --no-pager -n 100,定位到具体是解析 hosts 时出错,而不是端口占用或存储驱动问题。 - 检查 hosts 文件内容:
cat -A /etc/hosts,-A参数会把行尾符和特殊字符显示出来。结果发现有一行是192.168.1.100后面直接跟了换行,没有主机名。 - 检查文件编码:
file /etc/hosts,确认是 ASCII text 而不是 UTF-8 with BOM。 - 修复并验证:删掉那行残缺记录,保存后执行
systemctl daemon-reload && systemctl restart docker,服务恢复正常。
这里有个细节值得强调:systemctl restart docker之前一定要先daemon-reload,因为 systemd 缓存了旧的单元状态,不 reload 的话有时候重启仍然会失败,让人误以为问题没修好。
2.3 /etc/hosts 的合法格式到底长什么样
为了避免再踩坑,我把/etc/hosts的合法格式规则列清楚:
| 规则项 | 合法示例 | 非法示例 | 后果 |
|---|---|---|---|
| 每行至少三段 | 10.0.0.1 db.internal | 10.0.0.1 | 解析报错 |
| IP 与主机名用空格或 Tab 分隔 | 10.0.0.1 db | 10.0.0.1,db | 解析报错 |
| 主机名不含特殊字符 | db-node-1 | db_node$1 | 解析报错 |
| 注释以 # 开头 | # 这是注释 | 行内随意 # | 可能截断 |
| 文件无 BOM | 纯 ASCII/UTF-8 无 BOM | UTF-8 with BOM | 首行解析失败 |
提示:修改
/etc/hosts后,不需要重启网络服务,但如果你改的是 Docker 依赖的解析记录,务必重启 Docker 守护进程让配置重新加载。
3. daemon.json 里配置 hosts 的常见误区
3.1 daemon.json 根本没有 hosts 字段
这是新手最容易犯的错。很多人想"给所有容器统一加几条域名解析",于是想当然地在/etc/docker/daemon.json里写:
{ "hosts": ["10.0.0.1 db.internal"] }结果 Docker 启动直接报unknown field "hosts"或者failed to parse daemon.json。原因很简单:Docker 守护进程的配置文件里压根没有hosts这个顶层字段。容器级别的 hosts 注入只能通过docker run --add-host或者docker-compose.yml里的extra_hosts来实现,没有全局配置项。
那为什么有人会以为有?因为 Docker 的daemon.json里确实有一个dns字段,可以配置容器默认使用的 DNS 服务器。dns和hosts是两个完全不同的概念:DNS 负责域名到 IP 的查询,hosts 是本地静态映射表。混淆这两个概念,就会写出非法配置。
3.2 daemon.json 写错导致服务起不来的典型报错
当你往daemon.json里塞了非法字段,Docker 启动时的报错通常长这样:
failed to start daemon: unable to configure the Docker daemon with file /etc/docker/daemon.json: the following directives don't match any configuration option: hosts看到don't match any configuration option这个关键词,基本可以断定是daemon.json里出现了 Docker 不认识的字段。修复方法就是删掉非法字段,只保留官方支持的配置项。
我整理了一份daemon.json里和网络解析相关的合法字段对照表,方便你查阅:
| 字段名 | 作用 | 是否影响 hosts | 示例 |
|---|---|---|---|
dns | 容器默认 DNS 服务器 | 否 | ["8.8.8.8"] |
dns-search | 容器默认搜索域 | 否 | ["internal"] |
registry-mirrors | 镜像加速地址 | 否 | ["https://mirror.example.com"] |
insecure-registries | 允许 HTTP 的仓库 | 否 | ["registry.local:5000"] |
bip | bridge 网络网段 | 否 | "172.17.0.1/16" |
hosts | 不存在此字段 | - | 写了必报错 |
3.3 修改 daemon.json 的正确姿势
每次改daemon.json,我都建议按这个流程走,能避免 90% 的启动失败:
- 改之前先备份:
cp /etc/docker/daemon.json /etc/docker/daemon.json.bak - 用
python -m json.tool /etc/docker/daemon.json校验 JSON 语法,确保没有多余逗号、括号不匹配。 - 确认所有字段都是 Docker 官方支持的,不确定的字段先查文档再写。
- 执行
systemctl daemon-reload。 - 执行
systemctl restart docker。 - 用
docker info确认守护进程正常,并检查配置是否生效。
注意:
daemon.json对格式极其敏感,一个多余的逗号就能让整个 Docker 服务起不来。生产环境改这个文件前,最好在测试节点先验证一遍。
4. 容器内 --add-host 与 extra_hosts 的正确用法
4.1 --add-host 的语法和生效范围
容器级别的 hosts 注入,标准做法是docker run时加--add-host参数:
docker run -d \ --name myapp \ --add-host db.internal:10.0.0.1 \ --add-host cache.internal:10.0.0.2 \ nginx:latest这个参数会在容器启动时,把指定的映射写入容器内部的/etc/hosts。注意几个关键点:
- 格式是
主机名:IP,中间是冒号,不是等号。 - 可以重复使用多次,添加多条记录。
- 只对当前这个容器生效,不影响其他容器,也不影响宿主机。
- 容器重启后依然生效,因为这是容器配置的一部分。
如果你写成了--add-host 10.0.0.1:db.internal(IP 和主机名顺序反了),Docker 不会报错,但容器内解析会失败,因为格式被理解成了"主机名 10.0.0.1 对应 IP db.internal",而db.internal不是合法 IP。这种错误很隐蔽,容器能正常启动,只是解析不生效,排查时容易忽略。
4.2 docker-compose 中的 extra_hosts
用 compose 编排时,对应的字段是extra_hosts:
version: "3.8" services: app: image: myapp:latest extra_hosts: - "db.internal:10.0.0.1" - "cache.internal:10.0.0.2"这里有个版本差异要注意:Compose 文件格式 3.3 之前,extra_hosts用的是列表格式;3.3 之后也支持映射格式:
extra_hosts: db.internal: "10.0.0.1" cache.internal: "10.0.0.2"两种写法效果一样,但映射格式在 YAML 解析时对特殊字符更友好。我个人的习惯是统一用列表格式,因为它在不同 Compose 版本间兼容性最好,不容易因为版本升级出问题。
4.3 容器内 hosts 被覆盖的坑
有一种情况会让--add-host失效:容器启动后,某些初始化脚本或 entrypoint 直接重写了/etc/hosts。比如某些基础镜像的启动脚本会执行echo ... > /etc/hosts,把 Docker 注入的内容全部覆盖掉。
判断方法很简单,容器起来后进去看:
docker exec -it myapp cat /etc/hosts如果发现--add-host添加的记录不见了,那就是被覆盖了。解决办法有两个:一是改 entrypoint,用追加>>而不是覆盖>;二是把 hosts 配置放到容器启动后的初始化逻辑里,用脚本动态写入。
5. 当 Docker 服务已经起不来时怎么救回来
5.1 先让服务能启动,再谈配置
服务已经挂了的时候,第一优先级是恢复服务,而不是纠结配置对不对。我的处理顺序是:
- 临时移除可疑配置:把
daemon.json改名为daemon.json.bak,让 Docker 用默认配置启动。 - 恢复宿主机 hosts:如果怀疑是
/etc/hosts问题,先把它替换成一份干净的最小内容:
127.0.0.1 localhost localhost.localdomain ::1 localhost localhost.localdomain- 重启服务:
systemctl daemon-reload && systemctl restart docker。 - 确认服务正常:
docker info能正常输出,说明守护进程活了。 - 逐步加回配置:每次只加一个字段,重启验证,定位到具体是哪个配置项导致的问题。
这个"最小化恢复 + 逐步加回"的思路,比对着报错死磕效率高得多。因为 Docker 的报错信息有时候只告诉你"配置解析失败",不告诉你具体哪一行,逐步二分能快速锁定。
5.2 用 systemd 覆盖文件排查启动参数
还有一种情况是有人改了 Docker 的 systemd 单元文件,在ExecStart里加了--hosts之类的非法参数。这时候daemon.json是干净的,但服务照样起不来。
排查方法:
systemctl cat docker这个命令会显示 Docker 服务的完整单元配置,包括/etc/systemd/system/docker.service.d/下的覆盖文件。如果看到ExecStart里有奇怪的参数,那就是元凶。修复方式是删掉覆盖文件里的非法参数,或者直接删除整个覆盖文件,然后daemon-reload重启。
我见过最离谱的一次,是有人在ExecStart后面加了--add-host,以为这样能给所有容器全局加 hosts。结果 Docker 守护进程根本不认识这个参数,直接启动失败。记住:--add-host是docker run的参数,不是dockerd的参数。
5.3 启动失败后的日志定位技巧
Docker 启动失败的日志分散在几个地方,按优先级看:
| 日志来源 | 命令 | 能定位的问题 |
|---|---|---|
| systemd 状态 | systemctl status docker -l | 服务退出码、简要原因 |
| journal 日志 | journalctl -u docker -n 200 | 详细报错、解析失败字段 |
| Docker 自身日志 | tail -f /var/log/docker.log | 部分发行版才有 |
| 内核日志 | `dmesg | tail -50` |
大部分 hosts 相关的启动失败,journalctl -u docker就能给出明确线索。关键是看报错里有没有出现hosts、parse、invalid这些词。如果有,直接往宿主机 hosts 和 daemon.json 方向查。
6. 几个容易混淆的 hosts 配置场景对比
6.1 宿主机 hosts、容器 hosts、DNS 三者的边界
我把这三个概念用一个表格彻底区分开,这是理解所有 hosts 问题的基石:
| 维度 | 宿主机 /etc/hosts | 容器 /etc/hosts | DNS 服务 |
|---|---|---|---|
| 作用范围 | 整个宿主机 | 单个容器 | 网络内所有设备 |
| 配置方式 | 直接编辑文件 | --add-host/extra_hosts | 部署 DNS 服务器 |
| 是否影响 Docker 启动 | 会 | 不会 | 不会 |
| 修改后是否需重启 | 视情况 | 重建容器 | 无需 |
| 典型用途 | 本机域名映射 | 容器内服务发现 | 集群统一解析 |
理解这张表,你就能快速判断:如果 Docker 服务本身起不来,问题一定在宿主机 hosts 或守护进程配置;如果只是某个容器解析不对,问题在容器级配置。
6.2 什么时候该用 hosts,什么时候该上 DNS
很多人一遇到域名解析问题就想改 hosts,其实 hosts 只适合少量、静态、固定的映射。当你的服务数量超过十几个,或者 IP 会动态变化时,hosts 维护成本会急剧上升。这时候应该考虑:
- 小规模固定环境:hosts 够用,简单直接。
- 中等规模:用 Docker 内置的 DNS(自定义网络下的服务名解析)。
- 大规模动态环境:上独立的 DNS 服务或服务发现组件。
我个人的经验是,容器之间通信用 Docker 自定义网络的服务名解析最省事,根本不需要配 hosts。只有需要访问外部固定 IP 的内部服务时,才用--add-host补充。
6.3 自定义网络下的服务名解析优先于 hosts
这里有个优先级问题值得说清楚。当容器加入自定义 bridge 网络后,Docker 内置 DNS 会负责服务名解析。如果你同时用--add-host配了同名记录,哪个生效?
实测结果是:/etc/hosts的静态记录优先于 Docker 内置 DNS。因为 Linux 的解析顺序默认是先查/etc/hosts,再查 DNS。所以如果你--add-host db:10.0.0.1,同时网络里有个服务叫db实际 IP 是10.0.0.2,容器会解析到10.0.0.1。这个特性可以用来做解析覆盖,但也容易造成"明明服务在跑却连不上"的诡异问题。
7. 生产环境配置 hosts 的稳妥实践
7.1 变更前的检查清单
在生产环境动 hosts 相关配置前,我固定会过一遍这个清单:
- [ ] 确认要改的是宿主机 hosts 还是容器 hosts,两者影响范围完全不同。
- [ ] 宿主机 hosts 改动前先备份:
cp /etc/hosts /etc/hosts.$(date +%F)。 - [ ] 检查 hosts 文件编码和行尾符,确保没有 BOM 和 Windows 换行。
- [ ] 确认
daemon.json里没有非法字段,用json.tool校验过。 - [ ] 变更窗口内保留回滚方案,知道怎么一键恢复。
- [ ] 改完后用
docker info和实际容器解析测试双重验证。
7.2 用配置管理工具固化 hosts 内容
手工改 hosts 最大的问题是不可追溯、容易漂移。规模稍微大一点的环境,我都建议用配置管理工具(如 Ansible、SaltStack)来统一管理/etc/hosts内容。这样有几个好处:
- 所有节点的 hosts 内容一致,不会出现某台机器多一行少一行。
- 变更走版本控制,出问题能快速回滚。
- 新节点上线自动同步,不用手工维护。
即使不用专业工具,至少也要把 hosts 内容纳入版本管理,每次改动有记录。我踩过的坑就是某台机器的 hosts 被临时改过,半年后排查一个解析问题时才发现,浪费了大量时间。
7.3 监控 Docker 服务启动状态
最后一道防线是监控。Docker 服务启动失败如果没人发现,可能几个小时都没人知道。基础的监控项包括:
docker.service的 active 状态,非 active 立即告警。docker info命令的返回码,非零告警。- 节点上运行容器数量的突变,骤降说明可能出了问题。
这些监控配置起来不复杂,但能在故障发生时把发现时间从"用户报障"缩短到"分钟级"。我负责的环境里,Docker 服务状态是必监控项,因为它是所有容器化业务的底座,底座塌了上面全完。
8. 我踩过的几个真实坑和对应解法
8.1 用 echo 追加 hosts 时把文件写坏
有一次图省事,直接用echo "10.0.0.1 db" >> /etc/hosts追加记录。结果那条命令里的引号被 shell 处理掉了,实际写入的内容少了主机名,变成了一行只有 IP 的非法记录。当时没注意,第二天 Docker 重启就挂了。
教训是:追加 hosts 记录时,一定要用cat或printf确认写入内容,别盲目相信 echo。更稳妥的做法是用tee -a:
echo "10.0.0.1 db.internal" | sudo tee -a /etc/hosts这样写入的内容会同时打印到终端,肉眼能直接核对。
8.2 daemon.json 里多了一个逗号
JSON 不允许最后一个元素后面有逗号,但很多人从其他配置复制过来时会带上。我见过一个daemon.json:
{ "dns": ["8.8.8.8"], "bip": "172.17.0.1/16", }最后那个逗号让 Docker 直接启动失败,报错信息还特别含糊,只说failed to parse。后来养成习惯,每次改完都用python -m json.tool过一遍,这个坑就再没踩过。
8.3 容器内 hosts 被 entrypoint 覆盖
有个自研镜像的 entrypoint 脚本里有一行cat > /etc/hosts <<EOF,把 Docker 注入的 hosts 全冲掉了。表现是--add-host配了但解析不生效,排查了半天才发现是脚本的问题。后来改成cat >> /etc/hosts,问题解决。
这个坑的隐蔽性在于:容器能正常启动,--add-host参数也没报错,只是解析结果不对。如果不进容器看/etc/hosts的实际内容,根本想不到是被覆盖了。
8.4 宿主机 hosts 里的 IPv6 记录引发解析异常
有些系统默认的/etc/hosts里有::1 localhost这样的 IPv6 记录。如果 Docker 配置里对 IPv6 支持不完整,或者某条 IPv6 记录格式有问题,也可能导致守护进程启动异常。我遇到过一次,是因为手动加了一条fe80::1 db但没指定网卡,解析时行为不确定。后来把这条删掉就正常了。
经验是:宿主机 hosts 里尽量只保留必要的 IPv4 记录,IPv6 记录除非确实需要,否则不要手工添加。默认的那两行 localhost 记录保持原样即可。
9. 快速自查:你的 hosts 配置是否安全
如果你现在就想检查自己的环境有没有隐患,按下面这个流程走一遍,五分钟就能排查完:
- 检查宿主机 hosts 格式:
cat -A /etc/hosts,确认每行都有 IP 和主机名,没有残缺行。 - 检查文件编码:
file /etc/hosts,确认不是 BOM 开头。 - 校验 daemon.json:
python -m json.tool /etc/docker/daemon.json,确认语法正确。 - 检查 daemon.json 字段:确认没有
hosts这种非法字段。 - 检查 systemd 覆盖:
systemctl cat docker,确认ExecStart没有多余参数。 - 测试重启:在低峰期执行
systemctl restart docker,确认能正常起来。 - 验证容器解析:起一个测试容器,
docker exec进去cat /etc/hosts和ping一下目标域名。
这套流程我在每次重大变更前都会跑一遍,虽然看起来繁琐,但比起凌晨被叫起来救火,这点时间花得值。Docker 的 hosts 配置本身不复杂,复杂的是它横跨宿主机、守护进程、容器三个层面,任何一个层面出问题表现都不一样。把这三个层面的边界理清楚,大部分"配置 hosts 导致服务起不来"的问题都能快速定位和解决。