跳到正文
前端知识库
工程化

Docker 面试进阶详解:构建、排障与安全

围绕镜像构建、容器生命周期、网络存储、资源限制、内网访问和发布治理,整理 Docker 的可验证工程实践。

8 分钟Docker · 容器化 · DevOps · 发布 · 稳定性 · 安全 · 面试

Docker 面试进阶详解:构建、排障与安全

本文融合 25年下半年面试真题预测.pdf 第 2.15 节的 Docker 题目。保留可迁移的排障和治理方法,删除课程宣传;文中的命令用于说明排查思路,执行清理、网络暴露和权限变更前必须确认目标环境。

1. 从进程模型理解 Docker

Docker 不是一台轻量虚拟机。容器通常是由 Linux namespace、cgroups 和镜像文件系统隔离出的进程组,共享宿主机内核。这个差异决定了几个面试结论:

  • 容器隔离的是进程、网络、挂载和资源视图,不等同于硬件级安全边界;
  • 镜像层适合分发静态文件,运行时数据应放到 Volume、数据库或对象存储;
  • 容器崩溃后可以快速重建,但持久化、备份和迁移要由系统设计负责;
  • 一个容器通常运行一个主进程,多个职责应拆成可独立扩缩容的服务。
Dockerfile
  -> BuildKit 构建缓存
  -> 分层 Image(不可变发布物)
  -> Container(进程 + namespace + cgroup)
       |-- Network
       |-- Volume
       |-- stdout/stderr

2. 镜像构建:可复现比“能跑”更重要

2.1 多阶段构建和依赖缓存

Node 服务可以采用下面的结构。具体 Node 版本应与项目运行时和依赖兼容,并在生产中固定到经过验证的 tag 或 digest:

# syntax=docker/dockerfile:1
FROM node:22-alpine AS dependencies
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM dependencies AS build
COPY . .
RUN npm run build

FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

这样安排的原因:

  1. 依赖清单单独复制,源码变化时仍可复用安装层;
  2. 编译器、测试依赖和源码不进入最终运行镜像;
  3. npm ci 使用 lockfile,避免构建时悄悄升级依赖;
  4. 非 root 用户降低应用被利用后的影响范围;
  5. exec 形式的 CMD 让 Node 收到 SIGTERM,可以执行优雅退出。

生产构建还应生成 SBOM 或依赖清单,扫描高危漏洞,并把基础镜像、包管理器和构建工具的版本纳入变更记录。镜像 tag 可被重新指向,关键发布物应记录 digest。

2.2 .dockerignore 和秘密管理

.git
node_modules
coverage
.next
dist
*.log
.env*
Dockerfile*

.dockerignore 减小构建上下文,也避免把本地密钥和无关文件送入 BuildKit。不要把数据库密码、JWT 密钥或云凭据写进 Dockerfile 的 ARG、镜像层或仓库;构建密钥和运行时密钥应由 CI/CD 或编排平台注入,并限制读取范围。

3. 启动失败的排查顺序

3.1 Docker daemon 无法连接

典型错误是 Cannot connect to the Docker daemon。先判断是服务未启动、当前用户没有 socket 权限,还是 CLI 指向了错误的 context:

docker context show
docker info
docker ps

在 Linux 主机上再检查 daemon 服务和 socket 权限;在 Docker Desktop 等环境中则检查桌面引擎是否已启动。把用户加入 docker 组等同于授予接近 root 的主机控制能力,必须经过权限评估,不能作为无条件修复。

3.2 容器启动后立即退出

容器的生命周期跟随 PID 1。常见原因是入口命令执行完、配置错误、端口冲突或应用启动异常:

docker ps -a
docker logs --tail 200 <container>
docker inspect <container> --format '{{.State.ExitCode}} {{.State.Error}}'

不要使用 tail -f /dev/null 伪造“常驻进程”掩盖问题。应用应以前台进程运行,例如 Nginx 使用 daemon off;,Node 使用 node dist/server.js。容器收到停止信号后,应用要在 deadline 内关闭 HTTP server、连接池、定时器和消息订阅。

3.3 镜像拉取或构建超时

先区分 DNS、代理、认证、仓库限流和镜像本身体积:

docker system df
docker pull <registry>/<image>:<tag>
docker build --progress=plain .

企业环境可配置经过批准的私有镜像代理或 registry mirror。镜像加速地址会影响供应链信任和可用性,不能把网络上找到的镜像源直接写入生产 daemon 配置。大镜像应通过多阶段构建、精简依赖和合理的基础镜像降低拉取成本。

3.4 清理磁盘前先确认对象

docker system df
docker image ls
docker container ls -a
docker volume ls

image prune 可能删除未被容器引用的镜像,volume prune 可能删除看似未挂载但仍需要恢复的数据卷,system prune 影响范围更大。生产环境应先做备份和审批,按标签、年龄或明确 ID 清理,而不是直接执行全局 prune。

4. 网络故障与内网访问

4.1 端口、服务发现和 DNS

宿主机 127.0.0.1:8080 --publish--> 容器 app:3000
同一 user-defined network:
  frontend -> http://api:3000
  • 容器间调用优先使用 user-defined network 的服务名,不要把容器 IP 写死;
  • EXPOSE 不等于发布端口,-p 才会建立宿主机映射;
  • 只允许需要的端口和网络方向,管理端口不应暴露公网;
  • DNS 失败时在容器内检查 /etc/resolv.conf、默认路由和代理配置,确认是解析失败还是目标服务拒绝连接。

可以用临时工具容器做分层排查,但不要把公共 DNS 地址当成所有生产网络的默认修复:

docker run --rm --network container:<target> busybox nslookup api.example.com
docker run --rm --dns <approved-dns> busybox nslookup api.example.com

4.2 FRP 和 SSH 隧道的边界

PDF 展示了 FRP 的公网服务端、内网客户端和 HTTP/TCP 转发,也提示 SSH 隧道不安全。面试时应回答架构和风险,而不是背配置:

外部请求 -> 受控公网入口/FRP 服务端 -> 加密隧道 -> 内网代理 -> 目标容器

落地前要确认:

  1. 隧道两端使用强认证、最小权限和加密传输;
  2. 只暴露必要的域名、路径或端口,并限制来源 IP;
  3. 记录连接、转发和失败日志,设置空闲超时和带宽上限;
  4. 不把 Docker daemon socket、数据库管理端口或内部元数据服务直接映射公网;
  5. 优先使用组织批准的 VPN、零信任网关或云厂商内网连接方案。

临时 SSH 端口转发适合受控的调试窗口,不应作为长期生产发布架构。任何隧道都不能替代应用鉴权和资源级授权。

5. 资源限制与健康检查

5.1 CPU 和内存

容器不设上限时,单个进程可能争抢宿主机资源。排查时结合容器和主机指标:

docker stats <container>
docker top <container>

运行时可以设置 CPU、内存和进程数限制,例如:

docker run --cpus=1 --memory=512m --pids-limit=256 app:prod

限制不是性能优化本身。需要观察 OOM kill、GC、事件循环延迟、请求 P95/P99 和重启次数,并根据负载测试调整。内存限制过小会导致频繁 OOM,过大则可能掩盖泄漏。

5.2 健康检查和优雅退出

健康检查应区分“进程活着”和“可以接收流量”:

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -qO- http://127.0.0.1:3000/health/ready || exit 1

/health/live 通常只检查进程,/health/ready 可以检查必要依赖是否已连接。不要在健康检查中执行昂贵查询,也不要把所有外部依赖短暂抖动直接变成容器重启风暴。编排平台应给应用足够的 termination grace period,让连接排空和后台任务收尾。

6. 存储、日志和可观测性

存储

  • 临时文件可用 tmpfs,并设置大小上限;
  • 需要跨容器和重启保留的数据使用 named volume、数据库或对象存储;
  • bind mount 适合本地开发和明确的主机共享,不要默认把宿主机根目录挂进容器;
  • 数据库备份必须独立于 Volume 本身,Volume 不是备份策略。

日志和指标

应用日志写 stdout/stderr,用平台统一采集、轮转和检索。结构化日志至少包含时间、服务、版本、请求 ID、状态和耗时,敏感字段要在应用边界脱敏。指标可包括:

  • 容器重启、退出码和 OOM 次数;
  • CPU、内存、文件系统和网络使用率;
  • 请求吞吐、错误率、P95/P99 延迟;
  • 镜像拉取、部署和健康检查失败率。

日志、指标和链路追踪要能关联同一发布版本;只看 docker ps 不能证明服务健康。

7. 安全与供应链

  1. 基础镜像和依赖使用固定版本,定期重建获取安全修复;
  2. 运行时使用非 root 用户,删除不需要的 shell、包管理器和调试工具;
  3. 不在镜像、日志和命令行参数中暴露密钥;
  4. 使用镜像签名、漏洞扫描和 SBOM,审核第三方镜像来源;
  5. 限制容器 capabilities、挂载点、网络出口和 Linux 权限;
  6. Docker socket 具有主机级控制能力,除非明确隔离,否则不要挂载到业务容器;
  7. 对公网入口配置 TLS、鉴权、限流和审计,容器隔离不能替代应用安全。

8. 面试题答法

Q: 容器为什么启动后就退出?

A: 容器跟随 PID 1 的生命周期。先看 docker ps -a、退出码和日志,判断入口命令结束、配置错误、依赖不可用还是权限问题;让应用以前台进程运行,并实现信号处理和优雅退出,不能用假进程掩盖异常。

Q: 容器无法访问外网怎么查?

A: 分层检查容器网络、默认路由、DNS、代理、防火墙和目标服务。先在同一 network 用临时容器验证解析和 TCP,再看应用超时与证书;不要只修改 DNS,也不要把公共 DNS 当成生产通用方案。

Q: 如何优化 Docker 镜像?

A: 先用构建分析确认大层,再采用多阶段构建、缓存 lockfile、精简运行依赖、.dockerignore 和固定基础镜像;同时扫描漏洞、使用非 root 用户并验证运行时健康,不能只追求镜像体积。

Q: FRP 或 SSH 隧道能解决生产访问问题吗?

A: 它们解决的是网络可达性,不解决鉴权、审计和数据安全。生产应优先使用批准的 VPN/零信任入口,隧道只开放最小端口、加密认证、限时并监控,不能把 Docker socket 或数据库管理端口直接暴露。

Q: Docker 和虚拟机有什么区别?

A: 容器通常共享宿主机内核,通过 namespace/cgroup 隔离进程和资源,启动快、密度高但隔离边界不同;虚拟机包含独立内核,隔离和兼容性更强但开销更大。选型还要看安全等级、内核需求和运维能力。

9. 一套可执行的发布检查

构建可复现(lockfile、digest、SBOM)
-> 镜像扫描和非 root 检查
-> 资源、网络、Volume 和 secrets 边界确认
-> 健康检查、优雅退出和回滚演练
-> 日志/指标/trace 可关联版本
-> 灰度发布后观察错误率、延迟、OOM 和重启