IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。
Docker 早已不是"加分项",而是现代软件开发、测试、部署的必备技能。面试官不会只问你"Docker 是什么",而是会顺着一条条命令、一层层镜像、一个个网络策略深挖下去,直到触到你对内核隔离机制的理解。下面我以高频率、高区分度的真实面试题为线索,配以可运行的例子和深度解读,帮你构建完整的 Docker 知识地图。
一、基础概念:你究竟在"装"什么?
Q1:Docker 容器和虚拟机到底区别在哪?请从技术层面说明,别只说"轻量"。
很多候选人脱口而出"共享内核所以轻量",但当被追问"共享内核到底共享了什么"就卡住了。回答这个问题,需要落到 Linux 内核特性上。
例子 & 解读:
启动一个 Ubuntu 容器并查看它的"内核版本":
bash
docker run -it ubuntu:22.04 uname -r
你会发现打印出的内核版本与宿主机完全相同------容器根本没有自己的内核,它只是宿主机上一个被 namespace 隔离的进程组,并通过 cgroups 限制资源。而虚拟机里的 uname -r 会是虚拟机自带的 Guest OS 内核,由 Hypervisor 模拟完整硬件栈启动。
更深入一点:容器的"文件系统独立"靠的是联合文件系统(OverlayFS),进程隔离靠 PID namespace,网络隔离靠 net namespace。你可以直接验证 namespace:
bash
docker run -d --name web nginx
# 获取容器内 nginx 主进程在宿主机的 PID
PID=$(docker inspect web --format '{{.State.Pid}}')
# 查看该进程所在的 namespace 链接
sudo ls -l /proc/$PID/ns/
你会看到 pid, net, mnt, uts, ipc, cgroup 等 namespace 的符号链接,它们指向与宿主机不同的 namespace 实例。这就是隔离的"原材料"。面试中能把 namespace 做隔离、cgroup 做限制、UnionFS 做分层镜像 这三个内核支柱讲清楚,并给出验证命令,基本直接通过基础关。
二、镜像与分层:你的镜像为什么这么大?
Q2:写 Dockerfile 时,为什么推荐把 COPY 或 RUN apt-get update 这类命令放在一起?
这考察你对镜像分层与缓存的理解。Docker 镜像由多个只读层堆叠而成,每条 Dockerfile 指令(RUN/COPY/ADD)会生成一个新层。
例子:
糟糕写法:
bash
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y nginx
RUN apt-get install -y curl
优化写法:
bash
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y nginx curl && \
rm -rf /var/lib/apt/lists/*
为什么后者好?
-
层数减少:三个 RUN 变一个,减少元数据操作,加速推拉镜像。
-
缓存失效控制 :若分开写,当你增加第四个
RUN apt-get install -y vim时,前面的apt-get update层缓存可能早已过期,构建时会用旧缓存导致安装失败。合并后只要这条命令不变,缓存一直有效。 -
镜像体积 :在同一个 RUN 里清理
apt缓存才能真正删除,因为上一层产生的大文件即使在下层删除,也只是用"白障文件"标记隐藏,总镜像体积并不会变小。
你也可以用 docker history <image> 直观看到每一层的大小。如果一个层是 120MB,下一层"删除"了一个 100MB 文件,镜像总大小并不会减少 100MB。这种"层叠文件系统"的会计方式,是面试官最爱问的陷阱之一。
三、Dockerfile 指令的深度较量
Q3:CMD 和 ENTRYPOINT 的区别,不要背定义,给我一个场景必须用 ENTRYPOINT。
当面试官要场景,你就给他一个封装 CLI 工具的例子。
例子:
你想把自己的 Python 脚本打包成"命令行工具",让用户像这样使用:
bash
docker run my-tool --input data.csv --output result.json
Dockerfile 应该这么写:
bash
FROM python:3.11-slim
COPY tool.py /usr/local/bin/tool
RUN chmod +x /usr/local/bin/tool
ENTRYPOINT ["/usr/local/bin/tool"]
CMD ["--help"]
-
ENTRYPOINT定义了容器启动时必须执行的主程序。 -
CMD提供默认参数 ,可被docker run末尾的参数覆盖。
如果只用 CMD ["/usr/local/bin/tool", "--help"],用户执行 docker run my-tool --input data.csv 时,整个 CMD 会被替换成 --input data.csv,shell 找不到可执行文件直接报错。ENTRYPOINT 保证了工具本体不变,只追加或覆盖参数。
更深一层:ENTRYPOINT 有两种形式,exec 形式(JSON 数组)和 shell 形式(字符串)。exec 形式不会启动 shell,所以无法使用环境变量替换,但能正确接收 Unix 信号;shell 形式会在 /bin/sh -c 下运行,能展开变量但 PID 1 是 shell 而不是你的程序,导致 docker stop 信号无法被程序优雅处理。生产环境必须用 exec 形式。
四、网络:容器间怎么"说话"?
Q4:两个独立的自定义 bridge 网络中的容器,如何通信?为什么默认不能通?
Docker 的 bridge 网络默认通过 iptables 做了隔离。不同 bridge 网络之间不转发流量,这是有意为之的安全边界。
验证例子:
bash
# 创建两个网络
docker network create net1
docker network create net2
# 分别在两个网络中启动容器
docker run -d --name c1 --network net1 alpine sleep 3600
docker run -d --name c2 --network net2 alpine sleep 3600
# 进入 c1 ping c2 的 IP(先用 docker inspect 查 c2 IP)
docker exec c1 ping <c2_ip>
Ping 不通。因为宿主机的 iptables FORWARD 链默认丢弃了 net1 与 net2 之间的包。
打通方案:
将一个容器同时接入两个网络:
bash
docker network connect net2 c1
此时再 ping c2,通了。因为 c1 在 net2 网桥上也获得了一个 IP,路由可达。
解读:
面试官要的不是"能通/不能通",而是你对Docker 网络模型 的理解。Docker 使用 Linux bridge 和 iptables 构建网络虚拟化。每个自定义 bridge 网络对应宿主机一个 bridge 接口,有独立的子网,Docker daemon 会自动添加 iptables 规则只允许同网桥内的转发。你甚至可以用 iptables -L -n 在宿主机看到 DOCKER-ISOLATION-STAGE-2 链的存在。讲出这一步,足以证明你不仅会用命令,还知道它背后是标准的 Linux 网络栈。
五、存储与数据持久化:Volume 还是 Bind Mount?
Q5:数据库容器必须持久化数据,该用 Volume 还是 Bind Mount?为什么?
答案:首选 Volume。
例子 & 对比:
Bind mount:
bash
docker run -v /home/user/mysql-data:/var/lib/mysql mysql:8
Volume:
bash
docker volume create mysql-data
docker run -v mysql-data:/var/lib/mysql mysql:8
解读:
-
可移植性 :Volume 由 Docker 管理(
/var/lib/docker/volumes/),不依赖宿主机特定路径,跨平台迁移一致。Bind mount 绑定宿主机绝对路径,换一台机器目录结构不同就挂。 -
权限管理:Volume 初始化时,Docker 会将容器内目标目录的内容复制到空 Volume 中,而 Bind mount 直接遮蔽,可能导致容器找不到初始化的数据库文件,从而启动失败或权限错误。
-
性能:Volume 绕过联合文件系统,直接读写宿主机文件系统,性能与 Bind mount 相同,但 Volume 驱动可更换(如 NFS、cloud 存储),扩展性更强。
面试中要特别提一点:数据备份。Volume 的数据可以直接用临时容器打包:
bash
docker run --rm -v mysql-data:/data -v $(pwd):/backup alpine tar czf /backup/mysql-backup.tar.gz -C /data .
这种"用临时容器操作数据卷"的模式,在自动化运维中非常优雅,能体现你的工程思维。
六、Docker Compose 与多服务编排
Q6:docker-compose.yml 中 depends_on 真的能保证服务 A 先于服务 B 完全就绪吗?不能的话怎么解决?
depends_on 只控制容器启动顺序,不检查服务内部是否 ready。比如数据库容器启动了,但 MySQL 进程还在做数据恢复,应用容器已经尝试连接,导致 crash loop。
解决方案------带重试和健康检查:
bash
version: '3.8'
services:
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: root
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
retries: 10
start_period: 30s
app:
build: .
depends_on:
db:
condition: service_healthy
现在 depends_on 增加了 condition: service_healthy,应用必须等到数据库 healthcheck 连续返回成功才会启动。如果没有 Compose v3.4+ 支持的条件等待,还可以在应用启动脚本里使用 wait-for-it.sh 或在代码里实现带指数退避的重连逻辑。
解读:
这个问题挖的是你对分布式启动依赖的工程理解。容器编排的启动顺序是经典陷阱,面试官希望听到你如何将"容器启动"与"服务就绪"解耦------健康检查、重试机制、外挂初始化脚本,这些才是生产级的答案。
七、安全与资源限制
Q7:如何防止一个容器拖垮整台宿主机?具体怎么限制?
容器共享内核,若不限制资源,一个 fork 炸弹或内存泄漏就能导致宿主机 OOM。必须使用 cgroups 的 Docker 封装进行限制。
例子:
bash
docker run -d --name risky-app \
--memory="512m" \
--memory-swap="1g" \
--cpus="1.5" \
--pids-limit=100 \
--ulimit nofile=1024:2048 \
nginx
-
--memory:硬限制物理内存 512MB,超过会触发 OOM。 -
--memory-swap:内存+Swap 总量限制 1GB,设成与--memory相同值则禁用 swap。 -
--cpus:限制只能使用 1.5 个 CPU 核心的计算时间。 -
--pids-limit:容器内最大进程数,防止 fork 炸弹。 -
--ulimit:限制文件描述符数量,防止耗尽宿主机 fd。
解读:
不要只说"用 --memory 就行"。深入讲:Docker 背后通过 cgroup v1 或 v2 写入对应的限制文件,例如 --memory 最终体现为 /sys/fs/cgroup/memory/docker/<container-id>/memory.limit_in_bytes 的值。你可以进入宿主机的 cgroup 文件系统直接观察。还要提到 OOM 的调整:--oom-score-adj 可以在宿主机 OOM 时优先 kill 这个容器。这些细节会让面试官认为你是真正在生产中排过雷的人。
八、镜像优化与多阶段构建
Q8:一个 Go 程序的 Docker 镜像从 900MB 降到 15MB,你做了什么?
这是展示多阶段构建的经典题目。
原始写法:
bash
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["./myapp"]
Go 编译需要完整的工具链,但运行只需要一个静态二进制文件和系统最小依赖。
多阶段构建:
bash
# 阶段一:编译
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o myapp .
# 阶段二:运行
FROM scratch
COPY --from=builder /app/myapp /myapp
EXPOSE 8080
ENTRYPOINT ["/myapp"]
scratch 是一个空镜像,里面没有任何系统文件,连 shell 都没有。你复制进去的静态二进制成为唯一文件。最终镜像体积只等于你的二进制大小(~10-15MB),且攻击面极小。
解读:
多阶段构建让你只在需要时引入重型依赖(编译器、SDK),最终只拷贝必要的产物到干净镜像。面试中要强调:每个 FROM 开始一个新阶段,你可以给阶段命名(AS builder),后续用 COPY --from=builder 引用。这正是解决"构建时依赖"和"运行时依赖"解耦的最佳实践。你也可以举前端例子:Node.js 构建后只拷贝 dist 到 Nginx 镜像。思路一致。
九、Docker Daemon 与调试
Q9:docker run 后容器立刻退出,你如何定位原因?
面试官在考你容器生命周期 和调试三板斧。
场景模拟:
bash
docker run --rm my-app
# 无输出,直接退出,状态 Exited (0) 或 (1)
解读 & 步骤:
-
查看退出日志 :
docker logs <container>。如果是 Exited (1) 多半有错误输出。 -
覆盖入口点调试:如果应用本身没有输出,你可以覆盖 ENTRYPOINT/CMD 直接启动 shell 进去排查:
bashdocker run -it --entrypoint /bin/sh my-app在 shell 里手动执行启动命令,看报错。
-
检查退出码 :0 是正常退出,1 是应用错误,137 是被
kill -9(通常是 OOM 或docker stop超时强行杀掉),139 是 segfault,143 是优雅终止(收到 SIGTERM)。 -
不删除容器 :不要用
--rm,保留 Exited 容器,用docker inspect查看其状态、挂载、网络等详细信息。 -
信号处理问题 :如果容器
docker stop超过 10 秒才退出,说明应用没正确处理 SIGTERM。这是 Dockerfile 中 CMD 用了 shell 形式导致的常见病,改为 exec 形式即可解决。
十、Docker 在 CI/CD 与生产中的模式
Q10:如何实现零停机部署?描述一个基于 Docker 的滚动更新策略。
这考察的是你如何在生产中使用容器。以 Docker Swarm 或 Kubernetes 为例(即便你只提概念也能加分),核心点:
-
健康检查 :新容器启动后,必须经过
healthcheck判定为 healthy 才能接入流量。 -
滚动更新参数 :
--update-parallelism 2 --update-delay 10s控制一次更新几个容器及批次间隔。 -
反向代理:配合 Nginx/Traefik 动态 reload,先 weight 引流再摘除旧容器。
-
回滚 :
docker service rollback或者保留前一版本的镜像标签,确保可秒级回退。
简单编排示例(Docker Swarm 模式):
bash
docker service create \
--name web \
--replicas 3 \
--update-parallelism 1 \
--update-delay 10s \
--update-order start-first \
nginx:1.25
更新时:
bash
docker service update --image nginx:1.26 web
Docker 会逐个替换副本,新副本 healthy 后再替换下一个,实现零停机。
解读:
生产环境的部署不是"停旧启新",而是流量切换。你必须把应用设计成无状态、可并行运行,配置外挂到环境变量或配置中心,日志输出到 stdout/stderr 由 Docker 日志驱动收集,这样容器才能像"牲畜"一样被随时替换。这种云原生的设计哲学,是面试官判断候选人是否具备"大厂思维"的关键。
十一、更多深度问题
Q11:Docker 的几种网络模式分别用在什么场景?
例子 & 解读:
-
bridge(默认) :容器连接到
docker0网桥,通过 NAT 访问外网,适合单机容器通信。 -
host:容器直接使用宿主机网络栈,性能最高,但端口冲突风险大,适合高性能网络应用。
-
none:无网络,用于极度安全要求的批处理任务。
-
container:共享另一个容器的网络命名空间,常用于 sidecar 模式(如网络代理)。
-
overlay:跨主机容器通信,Swarm/K8s 基础。
创建 container 模式的例子:
bash
docker run -d --name web nginx
docker run -it --network container:web alpine sh
这时 alpine 容器看到的网卡、IP 与 nginx 完全一样,它们可以通过 localhost 通信。
Q12:自定义 bridge 网络和默认 bridge 网络的区别?
例子 & 解读:
默认 bridge (docker0) 不支持 DNS 自动解析,容器间必须用 IP 通信;自定义 bridge 网络内嵌 DNS 服务,可以直接用容器名互访。
测试:
bash
docker network create mynet
docker run -d --name web --network mynet nginx
docker run --rm --network mynet alpine ping web # 成功
换成默认 bridge 则 ping web 失败。面试中要强调 DNS 解析用的是内置的 127.0.0.11,内部 nameserver,跨网络无法解析。
Q13:Overlay 网络如何实现跨主机容器通信?
例子 & 解读:
Overlay 网络在多个 Docker 宿主机之间创建一个二层网络,利用 VXLAN 隧道封装数据包。初始化 Swarm 集群后创建 overlay:
bash
docker network create -d overlay --attachable my-overlay
容器发往另一台主机的包,被 VXLAN 封装,经宿主机物理网络传输,对端拆封装后送入目标容器。这是 Docker Swarm 和 Kubernetes 的底座。面试时可提一下 VXLAN、control plane 和数据面的分离,展示网络功底。
Q14:Docker 的日志驱动有哪些?如何把容器日志发送到 ELK?
例子 & 解读:
默认日志驱动是 json-file,存储在 /var/lib/docker/containers/<id>/<id>-json.log。若要集中采集,可改用 syslog、fluentd、journald、awslogs 等。
bash
docker run --log-driver=fluentd --log-opt fluentd-address=localhost:24224 nginx
或全局配置 /etc/docker/daemon.json:
bash
{
"log-driver": "fluentd",
"log-opts": {
"fluentd-address": "localhost:24224"
}
}
搭配 Filebeat 或 Fluentd 可无侵入收集。但要注意:切换日志驱动后,docker logs 命令可能无法查看历史日志。
Q15:Docker 存储驱动 Overlay2 的原理和优势是什么?
解读:
Overlay2 利用 Linux OverlayFS,将多个目录层叠成一个视图。镜像层为 lowerdir,容器层为 upperdir,合并后为容器文件系统。相比 AUFS,Overlay2 已是主线内核特性,性能更好,inode 消耗少。使用 docker info | grep "Storage Driver" 查看当前驱动。避免使用 devicemapper 等老驱动。
Q16:什么是 Docker Content Trust (DCT)?有什么用?
例子 & 解读:
DCT 使用数字签名确保镜像来源和完整性。启用后,docker pull 只会拉取经过签名的镜像:
bash
export DOCKER_CONTENT_TRUST=1
docker pull alpine:latest
如果镜像未签名,会拒绝拉取并报错。这防止镜像被中间人篡改。企业级安全必备。
Q17:如何扫描 Docker 镜像安全漏洞?
例子 & 解读:
使用 docker scan(基于 Snyk)或第三方工具如 Trivy:
bash
docker scan myapp:latest
# 或
trivy image myapp:latest
扫描镜像内软件包版本,对比 CVE 数据库,输出漏洞等级。面试中应提到:基础镜像选择 distroless 或 Alpine 可大幅减少攻击面;CI 中集成扫描可做卡点。
Q18:除了多阶段构建,还有哪些缩小镜像体积的方法?
例子 & 解读:
-
使用更小的基础镜像:如
alpine,甚至scratch。 -
合并 RUN 指令并清理临时文件。
-
安装包时用
--no-install-recommends(apt)或--no-cache(apk)。 -
用
.dockerignore排除.git、node_modules等。 -
使用
docker-slim等工具进行镜像瘦身。 -
将多个镜像层 squash 成一层的实验性功能(
docker build --squash)。
Q19:构建上下文过大导致 docker build 很慢,怎么排查和优化?
解读:
构建前 Docker 会把整个上下文目录(通常为项目根目录)打包发给 Daemon。若目录中包含大量文件(如 node_modules、日志、数据),会导致传输慢、镜像层膨胀。使用 .dockerignore 排除无关文件,或调整 Dockerfile 路径:
bash
docker build -f docker/Dockerfile -t app .
并确保上下文路径最小化。
Q20:docker stop 有时要等 10 秒,为什么?怎么改?
例子 & 解读:
docker stop 发送 SIGTERM,等待应用优雅退出(默认 10 秒),超时后发 SIGKILL 强杀。若应用没正确处理 SIGTERM(如 CMD 用 shell 形式),就会持续到超时。修改等待时间:
bash
docker stop -t 5 <container>
更根本的解决:确保 Dockerfile 中 CMD/ENTRYPOINT 使用 exec 形式,让应用直接成为 PID 1 接收信号。
Q21:如何安全地传递敏感信息给容器?
例子 & 解读:
不要用环境变量传密码(会留在 docker inspect 和日志中)。Swarm 模式使用 secrets:
bash
echo "dbpass" | docker secret create db_pass -
docker service create --secret db_pass myapp
非 Swarm 可用 Docker Compose 的 secrets 定义,或挂载只读文件(Volume/Bind Mount 权限 400)。运行时也可用 Vault 等外部密钥管理。
Q22:Docker Compose 中 extends 和 profiles 怎么用?
解读:
extends 可复用其他 Compose 文件的服务定义(已在新版本中用 include 替代)。profiles 则允许按需启动服务组:
bash
services:
debug:
image: busybox
profiles: ["debug"]
正常 docker compose up 不会启动 debug,需显式指定:
bash
docker compose --profile debug up
这在开发、调试场景中隔离不同服务集合很有用。
Q23:--privileged 和 --device 的区别?什么时候避免用特权模式?
例子 & 解读:
--privileged 赋予容器所有内核能力并解除设备限制,等于把容器 root 几乎等同于宿主机 root,极度危险。--device 仅映射特定设备文件:
bash
docker run --device=/dev/video0:/dev/video0 mycam
在生产中禁止 --privileged,改用细粒度的 --cap-add 添加所需内核能力,遵循最小权限原则。
Q24:如何远程安全访问 Docker Daemon?
解读:
开启 TCP 端口监听:
bash
dockerd -H tcp://0.0.0.0:2376 -H unix:///var/run/docker.sock \
--tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem
必须配置 TLS 双向认证,避免暴露无保护的 API(被植入挖矿等)。客户端使用对应证书连接。
Q25:一个容器需要运行多个进程怎么办?
例子 & 解读:
容器设计哲学是单进程,但现实中需要辅助进程(如 cron、agent)。可以使用 tini 作为 init 进程(--init 参数)处理僵尸进程,或用 supervisord 管理:
bash
FROM ubuntu
RUN apt-get update && apt-get install -y supervisor
COPY supervisord.conf /etc/supervisor/conf.d/
CMD ["/usr/bin/supervisord"]
更好的做法是拆分成多个容器,通过共享卷或网络通信。
Q26:如何备份和恢复 Docker Volume 数据?
例子 & 解读:
备份:用一个临时容器挂载 volume 和宿主机目录,执行打包:
bash
docker run --rm -v db_data:/data -v $(pwd):/backup alpine tar czf /backup/backup.tar.gz -C /data .
恢复:
bash
docker run --rm -v db_data:/data -v $(pwd):/backup alpine tar xzf /backup/backup.tar.gz -C /data
Q27:容器内时间和宿主机不同步怎么办?
解读:
容器通常共享宿主机时钟,但若没权限修改系统时间(需要 SYS_TIME 能力)。可挂载宿主机 /etc/localtime 解决时区:
bash
docker run -v /etc/localtime:/etc/localtime:ro ...
或设置环境变量 TZ=Asia/Shanghai(依赖于镜像支持)。但要注意,这并不改变内核时钟,仅影响用户态库。
Q28:排查 "no space left on device" 错误(磁盘满了)的路径
解读:
可能原因:容器日志过大、未清理的悬空镜像和卷。
-
清理:
docker system prune -a --volumes -
查看磁盘占用:
docker system df -
日志限制:在 daemon.json 中配置每个容器日志最大大小和轮转:
bash
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
- 检查 overlay2 层是否堆积。
Q29:怎样构建多平台镜像(amd64, arm64)?
例子 & 解读:
使用 docker buildx:
bash
docker buildx create --name mybuilder --use
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:multi-arch --push .
buildx 利用 QEMU 模拟或交叉编译,一次打出多架构镜像,适合边缘计算和树莓派等场景。
Q30:Docker、containerd 和 runc 之间的关系是什么?
解读:
这是经典的容器运行时分层:
-
runc:OCI 规范的低层运行时,负责创建和运行容器(基于 libcontainer)。
-
containerd :管理容器的生命周期、镜像传输、存储,为上层提供 gRPC 接口。
docker守护进程通过 containerd 启动容器。 -
Docker:面向用户的高层平台,包括 CLI、API、镜像构建、编排等。
可以用命令 docker info | grep -i runtime 查看。Kubernetes 也可以跳过 Docker 直接通过 CRI 接 containerd,这就是"弃用 Docker shim"的原因。
这些问题覆盖了 Docker 面试的绝大部分核心点:从基础概念、镜像优化、网络存储、安全限制,到生产实践、问题排查和架构理解。每一个答案都包含可验证的示例和背后的原理,帮你在面试中不仅有话说,更说得深、说得准。
想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !