自制Docker镜像与容器化部署全链路实战指南
做开发这些年被环境问题坑过的次数可能比写业务代码还多。本地跑得好好的一到服务器就崩换台新机器光装依赖就得折腾半天测试环境一套行为、生产环境一套行为两边对不上号。Docker 出现以后这类问题才算真正有了解法把应用和它运行所需的操作系统组件一起打成镜像发布到目标机器上直接跑容器环境不一致的问题从根上被掐掉。不过很多人平时用 Docker 就是 pull 一个公共镜像、run 一下真要按自己的需求做定制镜像把服务正式部署上线时才会发现对 Dockerfile 的精髓、镜像分层机制、容器运行参数理解得不够深踩起坑来效率很低。这篇就围绕“制作自定义 Docker 镜像并部署使用”这件事讲清楚从需求拆解、镜像构建到部署运行、跨机器交付再到故障排查的完整链路。适合刚把 Docker 用起来但想往前走一步的开发者也适合正在为团队做环境标准化的运维同学。1. 先想清楚再动手定制镜像到底解决什么问题1.1 现成镜像搞不定的具体场景直接用官方或者第三方现成镜像确实省事但大部分真实项目最终都逃不过定制这一步。我梳理了一下自己经手过的定制需求基本就是下面几类。第一类是应用本身的打包。代码在自己的仓库里依赖散落在各种语言生态中官方镜像里不可能存在你的业务应用这一步没什么可说的必须自己做镜像。第二类是运行环境预置。基础镜像自带操作系统但往往缺你需要的编译工具、字体、时区配置、地域设置、证书或者公司内部要求的初始化配置。与其每次启动容器时在入口脚本里现场安装不如一次性固化到镜像中启动即用。第三类是依赖组合锁定。比如一个 Python 推理服务需要 Python 3.10、PyTorch 特定版本再配合一堆数据处理库官方 Python 镜像不会内置这些东西如果你靠自写脚本在容器里现场装一旦版本发生漂移线上跑崩的概率相当高。把这些依赖装进镜像版本就被锁死了。第四类是安全合规要求。很多公司的容器基线会要求镜像内不能有高危漏洞、不能用 root 启动、不允许残留包管理器缓存。公共镜像是很难满足这些条件的只能基于精简基础镜像自己加层控制。最后一类是跨架构复用。同样一套应用既要跑在 x86 服务器上又要跑在 ARM 开发板比如 RK3588、Jetson 这类边缘设备上做推理任务。如果直接用别人打包的现成镜像经常会碰到架构不匹配、依赖编译失败的问题这时候自制镜像反而是唯一靠谱的路。如果你只是临时跑个 MySQL、Redis直接用官方镜像完全没问题。只要涉及自有代码、私有依赖、安全基线、多架构需求自制镜像几乎就是必经之路。1.2 定制之后获得的实际收益很多人会把“做镜像”理解成多了一步工作量但实际上它换来的是整套交付方式的改变。我用一个对比表格给你看看差异对比项直接使用现成镜像 手动装环境自制镜像 容器化部署环境一致性每台机器手工装版本容易漂移镜像构建一次处处可跑启动速度容器首次启动要现场装依赖慢则几分钟依赖都在镜像里秒级启动版本回溯往往是 latest 流时间一长根本不知道跑的是哪个版本打上精确 tag随时回滚安全基线镜像内包管理缓存、root 用户等问题难以控制可主动精简依赖、固定非 root 用户交付链路手工拷代码、装环境易出错镜像作为单一交付物CI/CD 直接流转镜像一旦做好它就已经不是“代码”了而是“可交付产物”。你可以在持续集成流水线里构建、扫描、签名、推送部署时只是把某个 tag 的镜像拉下来跑起来。回滚就更简单了把容器换成上一个 tag 的镜像重启即可。这种收益在项目规模上来之后会非常明显也是团队做环境标准化的关键基础。2. Dockerfile 不是照着抄就行构建原理与指令细节2.1 镜像分层机制理解它是后续所有优化的前提一开始学 Dockerfile很多人都是网上找一份照着改。结果发现别人的镜像几百 MB自己的镜像几个 GB别人改了代码构建很快自己一改代码就要重新装十几分钟依赖。这些差异根源都在“镜像分层”这个机制上。镜像不是一个大文件而是由若干只读层叠加出来的。你可以把它类比成 git 的提交历史Dockerfile 中每一条指令会生成一个层记录的是这条指令造成的文件系统差异上一条指令的结果是下一条指令的输入。容器运行时最上面还会叠加一个可写层容器内产生的改动全部写在这一层但容器一旦删除可写层也跟着消失底层镜像本身不受影响。这套机制直接决定了三件很重要的事情。缓存复用构建时如果某一条指令没有变化Docker 会直接复用之前的层不会重新执行。所以把不经常变化的操作放在前面比如系统依赖、Python 依赖把频繁变化的代码 COPY 放在最后这样每次改代码构建时只有最后几层需要重做。删除文件不一定减小体积一个文件只要写进过某个层即使后面层把它删掉它仍然存在于镜像历史里镜像体积并不会变小。所以apt-get install之后清理缓存和删除临时文件必须放在同一条 RUN 指令里确保这些操作和安装发生在同一层。层数少的镜像不等于体积小体积看的是所有层的内容总和而不是层数。合并指令主要是减少构建步骤和提升可读性对体积的优化效果有限。理解了分层你才能真正看懂多阶段构建、缓存失效这些问题。2.2 常用指令的行为差异与正确用法Dockerfile 指令不算多但每条指令的行为细节都有讲究。我把常用的一套整理成速查表再挑几个最容易出错的展开说。指令关键行为实际使用建议FROM指定基础镜像优先选择 slim、distroless、alpine 等精简版本ARG构建时变量仅在构建阶段可用不会带入镜像运行环境ENV环境变量会固化到镜像环境里运行时可被覆盖WORKDIR设置当前工作目录始终使用避免在根目录下乱放文件COPY从构建上下文复制文件复制文件用这个行为简单可预期ADD复制自动解压需要解压 tar 时再用不要用 ADD 获取远程 URLRUN执行命令并生成新层使用时注意合并清理动作CMD指定默认启动命令可被docker run后面的命令覆盖ENTRYPOINT指定固定入口定制服务推荐用 ENTRYPOINT 固定CMD 提供默认参数USER切换用户生产镜像必须切换到非 root 用户EXPOSE声明端口只是文档声明不真的发布端口HEALTHCHECK定义健康检查命令生产服务强烈建议配置重点说三个容易踩坑的点。第一CMD有两种写法。exec 形式是CMD [python, app.py]shell 形式是CMD python app.py。两者的差别在于进程的 PID 编号和信号处理。exec 形式会让 python 直接成为容器内 1 号进程Docker 停止容器时能正确把退出信号传给它shell 形式则会让/bin/sh -c成为 1 号进程信号传递会出现一层隔阂导致优雅停机失效。写生产镜像一定要用 exec 形式。第二COPY和ADD的区别。ADD除了复制文件还会检测源文件是否为 tar 包并自动解压也能从远程 URL 拉文件。听起来很强大但在实际工程里自动解压不一定是你想要的而远程 URL 拉取会因为网络问题让构建不稳定。所以我的原则很简单普通复制一律用COPY只有明确需要解压 tar 包时才用ADD远程拉取则应该放在RUN中使用 curl 或 wget至少失败时可重试。第三ARG和ENV很容易混。ARG只在构建阶段存在不会进入最终镜像的环境变量ENV则会固化到镜像中运行时可以通过docker run -e覆盖。如果某个参数既要参与构建又要影响运行时行为那就要分别定义或者在构建阶段通过ENV输出别指望ARG会在容器运行时出现。2.3 构建上下文与 .dockerignore最容易被忽略的一环docker build执行时会把当前目录或你指定的目录整个打包发送给 Docker 守护进程这个目录叫构建上下文。Dockerfile 里所有COPY指令都只能引用这个目录里的文件。很多人以为COPY ./config /app/config就是把 Dockerfile 所在目录发过去其实不完全对Docker 守护进程拿到的是构建上下文压缩包而不是直接读你本地文件。这个机制的代价就是如果你构建目录里塞了 node_modules、.git、日志、临时文件它们会完整地被打包发送。构建慢是小事更恶心的是这些图片可能进入构建上下文导致后续层缓存膨胀甚至因为权限问题直接构建失败。所以每个项目根目录都应该有一份.dockerignore作用类似于.gitignore。一个比较通用的模板是这样.git .vscode .idea __pycache__ *.pyc *.pyo node_modules dist build *.log .tmp .env.local注意.dockerignore中漏掉.git是最常见的问题。我曾经见过一个项目Dockerfile 本身没问题但构建时把整个 .git 目录几十 MB 历史一起发过去了每次构建传输都慢得离谱。加上.git忽略之后构建时间立刻降了 80%。另外还要提醒一句不要在构建上下文里放多余的大文件。如果镜像只需要某个子目录构建时可以在 docker build 命令里指定子目录作为上下文或者干脆把上下文收敛到最小范围。这个动作对构建速度和安全性都有帮助。3. 实战演示从零构建一个 Python 推理服务镜像3.1 需求与基础镜像选型理论讲再多不如直接看一个实际例子。假设我要做一个推理服务Flask 提供 HTTP API内部加载一个训练好的模型做预测最后跑在一台 x86 Linux 服务器上。基础镜像选什么这里我直接给出答案python:3.10-slim。为什么不用python:latest因为 latest 语义不可控今天构建和三个月后构建拿到的可能是不同版本这违背了镜像可复现的原则。为什么不用alpine因为很多 Python 包没有预编译的 musl 版本比如某些科学计算库在 alpine 上需要现场编译不仅慢还容易失败slim 版本基于 glibc和大部分预编译轮子兼容性好体积也只有完整官方镜像的三分之一左右。至于系统依赖要根据应用的实际需要来加。为了演示多阶段构建的效果我在第一阶段装了编译工具运行时阶段只保留了运行所需的动态库。这个思路对于大多数需要编译依赖的项目都适用。3.2 Dockerfile 逐段拆解下面是我会在项目里使用的多阶段 Dockerfile先整体看一下# ---------- 第一阶段安装依赖 ---------- FROM python:3.10-slim AS builder ENV PIP_DISABLE_PIP_VERSION_CHECK1 \ PIP_NO_CACHE_DIR1 RUN apt-get update \ apt-get install -y --no-install-recommends build-essential libgomp1 \ rm -rf /var/lib/apt/lists/* COPY requirements.txt /build/requirements.txt RUN pip install --prefix/build/deps -r /build/requirements.txt # ---------- 第二阶段精简运行镜像 ---------- FROM python:3.10-slim AS runtime RUN apt-get update \ apt-get install -y --no-install-recommends libgomp1 \ rm -rf /var/lib/apt/lists/* \ useradd --create-home --uid 10001 appuser COPY --frombuilder /build/deps /usr/local COPY app /app WORKDIR /app USER appuser EXPOSE 8000 CMD [python, app.py]这个文件不长但每一个细节都是有讲究的我把关键点拆给你看。第一段是编译阶段。基础镜像拉了 build-essential这是为了确保那些需要现场编译的 C 扩展包能够顺利构建。--no-install-recommends可以避免安装不必要的推荐包能明显减少安装体积。注意rm -rf /var/lib/apt/lists/*必须和apt-get install在同一条 RUN 指令里这样清理动作处于同一个层不会留下 apt 索引缓存。然后是依赖安装。我先把requirements.txt单独 COPY 进来是为了利用 Docker 缓存机制只要 requirements.txt 不变这一层就会被复用后续构建时 pip 安装不会重新执行。如果先把整个项目 COPY 再安装依赖那么代码一改依赖层缓存就全部失效这是新手最容易犯的错误。第二阶段就是运行环境。我没有从第一阶段继承整个文件系统而是重新 FROM 了同一个基础镜像只从 builder 阶段把安装好的 Python 依赖 COPY 过来。这样运行阶段就不需要 build-essential 等编译工具镜像体积会小很多。libgomp1是很多科学计算库运行时的 OpenMP 支撑库如果你不需要可以去掉但演示里把它留下来也顺便告诉你运行环境缺什么动态库要靠实际启动测试验证不能靠猜。最后创建了一个普通用户 appuser设置了固定的 UID然后切换过去。容器内没有密码管理、没有编译工具即使被攻破影响面也小得多。EXPOSE 8000只是声明真正暴露端口要靠运行时的-p参数。最后的CMD使用 exec 形式保证 app.py 作为主进程运行。3.3 多阶段构建能把镜像压小多少多阶段构建最直观的收益就是体积。如果你把完整的推理依赖、编译工具全塞进一个阶段镜像体积我可以告诉你现实中的数字一个带 PyTorch CPU 版本、若干依赖包的镜像堆到 800MB 到 1GB 是家常便饭。其中很大一部分是编译工具链比如 gcc、make、各类头文件这些在运行时根本用不到。多阶段构建之后体积通常能压到 400MB 到 500MB 左右如果再用上体积更小的运行时基础镜像、清理 pip 缓存和临时文件还能进一步压缩。体积小带来的好处不仅是磁盘占用少还意味着拉取时间短、启动时间快、被攻击面少。我建议你自己构建完后用docker image history --no-trunc看看每一层的大小把那些明显不属于运行时的包挑出来。只要你有“哪些东西只在构建时需要”的意识其实很多体积优化都不需要额外工具。3.4 构建过程常见的翻车点构建这个 Dockerfile 时有几个点我几乎每次做演示都会看到别人踩。第一apt-get和缓存清理不在同一条指令里。有人会在一个RUN里安装然后在下一个RUN里清理最后发现镜像体积一点没减。原因就是删除操作发生在新的层但之前层里的 apt 缓存还留在镜像历史中。记住安装和清理必须同层。第二USER appuser之后应用写文件就会遇到权限问题。比如模型要往缓存目录写数据目录归属还是 root运行时直接 IOError。解决办法有两个在构建阶段用RUN mkdir -p /data/cache chown -R appuser:appuser /data/cache或者在运行时用命名卷挂载。这是很多人在本地跑没问题、上了容器就报权限错误的根源。第三没有 HEALTHCHECK。如果你做的是一个 API 服务镜像构建成功不代表进程就在正常服务。只要配置一个简单的健康检查后续编排工具就能依赖它做自动恢复这个投入产出比极高。第四端口和工作目录没有确认。构建完了不验证直接推到仓库到了部署时才发现启动命令找不到文件或者端口冲突那就要回退好几个步骤。后面我会专门说验证这一步该怎么操作。4. 部署运行镜像变成容器的那些关键参数4.1 docker run 的选项应该这样选镜像构建完成下一步就是运行容器。很多人习惯docker run -d -p 8000:8000 镜像名一把梭这在本地玩玩没问题但是到了生产环境差一个选项可能就是一个事故。一个相对完整的运行命令长这样docker run -d \ --name inference-api \ -p 127.0.0.1:8000:8000 \ -v /data/models:/models:ro \ -v app-logs:/var/log/app \ -e APP_ENVprod \ --restart unless-stopped \ --memory 2g \ --cpus 2 \ registry.example.com/inference-api:v2.1.0每个选项我都拆开解释一下。-p 127.0.0.1:8000:8000表示只把容器的 8000 端口发布到宿主机的回环地址上也就是只有本机进程能访问公网访问不到。这个习惯非常重要尤其当你的服务没有内部门禁时公开映射等于直接把端口暴露到公网扫描机器人几分钟就能打上来。真正对外的访问应该由反向代理网关完成容器只绑 127.0.0.1。-v /data/models:/models:ro是把宿主机的模型目录以只读方式挂载进容器。这样模型可以独立于镜像迭代不用每次更新模型都重新构建镜像。:ro是只读保护防止容器内部误删或篡改模型文件。第二个-v app-logs:/var/log/app用的是命名卷它不关心宿主机具体路径交给 Docker 管理。应用日志必须写到这种持久化卷里否则容器一删除日志就没了后面排查问题就是两眼一抹黑。-e APP_ENVprod注入环境变量。注意不要把数据库密码这类敏感信息直接写在这里更不要写进 Dockerfile。生产环境应该配合配置中心或运维平台的密钥管理能力把环境变量注入这件事托管给平台。--restart unless-stopped表示进程崩溃或服务器重启后Docker 会自动拉起容器。这个策略比always更合适因为它不会把你手动停掉的容器强行再拉起来。--memory 2g --cpus 2是资源限制。你永远不希望一个内存泄漏的模型服务把宿主机整个拖垮。给每个容器框定资源上限是对同机其他服务最基本的保护。4.2 用 docker compose 把多个服务一起拉起来单个容器用docker run还能接受稍微上一点规模多个服务之间的网络互通、顺序启动、卷定义、健康检查都会变得难以维护。这时候我建议直接用 compose 文件把服务编排好版本管理也方便。还是用推理服务举例假设它同时依赖 Redis 做缓存compose 文件大致是这样的services: inference: image: registry.example.com/inference-api:v2.1.0 ports: - 127.0.0.1:8000:8000 volumes: - /data/models:/models:ro - app-logs:/var/log/app environment: APP_ENV: prod REDIS_URL: redis://redis:6379/0 restart: unless-stopped depends_on: redis: condition: service_healthy healthcheck: test: [CMD, python, -c, import urllib.request; urllib.request.urlopen(http://127.0.0.1:8000/health)] interval: 30s timeout: 5s retries: 3 redis: image: redis:7-alpine restart: unless-stopped healthcheck: test: [CMD, redis-cli, ping] interval: 10s timeout: 3s retries: 5这里我想重点说两个很多教程都不会讲清楚的细节。第一个是depends_on。它只保证服务的启动顺序不保证依赖服务已经“可用”。比如 Redis 先启动了但还在初始化inference 一启动连接就直接失败。所以在 compose 中要配合 healthcheck用condition: service_healthy来真正等待 Redis 健康检查通过。这个组合才是现代编排的正确姿势。第二个是服务名即 DNS。compose 会在内部创建一个默认网络服务之间可以用服务名互相解析比如redis、inference就是网络内的主机名。代码里的连接地址不要写127.0.0.1要写服务名。很多第一次用 compose 的人都栽在“容器内访问不到本机服务”上原因就是忘掉了容器环境的主机名规则。4.3 生产部署必须做好的几件事容器跑起来不等于部署成功。我在一线看过的容器化事故大部分都不是镜像本身的问题而是部署时少了一些关键配置。下面这几条是我觉得值得反复强调的。第一不依赖latest。镜像 tag 必须是不可变的版本号比如v2.1.0。一旦团队里有多个实例每个人 pull latest 拿到不同的镜像线上行为就会变得不可预测。tag 在 CI 里生成和代码提交记录一一对应这才是可追溯的基础。第二日志一定要限制大小和轮转。docker 默认的 json-file 日志驱动会把容器输出全部攒到一个文件里时间一长可以轻松占用几十 GB 磁盘。在 daemon.json 里配置max-size: 20m和max-file: 5或者干脆使用更专业的日志驱动否则迟早会撑爆磁盘。第三不要把数据库这类有状态服务的数据放在容器可写层。容器一旦被删数据就没了而且容器升级时很难迁移。正确做法是使用挂载卷或外部数据库实例。第四部署前必须做一次启动验证。我见过太多人把镜像推到仓库就以为万事大吉结果线上一启动就崩日志一看才发现启动命令写错。后面第 6 部分我会专门讲怎么排查这类问题这里先提一个建议每个镜像构建完后用docker run --rm -it --entrypoint sh手动起来看一眼再决定是否推送。5. 镜像跨平台交付保存、传输与多架构构建5.1 镜像体检与瘦身不要只看 docker images 显示的体积镜像做出来之后第一件事是体检而不是直接推送。很多人只看docker images输出的大小其实这个大小是“压缩传输前”的近似值真正要看的是每一层的内容和组织方式。查看每层大小可以用docker image history --no-trunc 镜像名能看到每一层由哪条 Dockerfile 指令产生、占多少空间。如果发现有哪个层体积异常大就追溯对应指令看里面有没有到底装了什么不该进运行镜像的包。想看更直观的图形分析可以试试 dive 这个命令行工具。它会给出每层新增文件的列表标注哪些文件可以删除对优化镜像体积很有帮助。我一般用它的流程是先看有没有大文件残留在缓存目录再确认有没有多个层反复写入同一个路径。分析出来后回到 Dockerfile 做对应的层合并或者清理优化重新构建一次。还有一句提醒docker system df可以看当前机器的镜像、容器、构建缓存分别占用多少空间。定期清理docker builder prune、docker image prune是本地磁盘不会突然爆满的必要习惯。5.2 没有私有仓库时镜像怎么跨机器传输不是所有环境都允许你搭一个私有仓库尤其是一些隔离的内网项目或者临时联调现场。这时候把镜像从一台机器弄到另一台机器docker save和docker load就是最标准的操作。一个比较稳妥的操作组合是docker save my-service:v1.0 | gzip my-service-v1.0.tar.gz scp my-service-v1.0.tar.gz roottarget-host:/tmp/ ssh roottarget-host gunzip -c /tmp/my-service-v1.0.tar.gz | docker loaddocker save会把镜像的所有层打包成一个 tar 归档管道接力到 gzip 压缩传输和加载时再解压。这套操作最大的优点是完整保留了镜像的元数据、层级关系和 tag 信息。这里我要郑重提醒一个小坑不要用docker export和docker import来迁移镜像。export导出的是运行中容器的文件系统快照会把容器的可写层也一并打进去同时丢失镜像的层级信息、环境变量、默认命令等元数据。导出的东西是“不干净的”import 回来极有可能无法按原样运行。可靠的原则是镜像迁移只有save/load和 push/pull 两种方式其他都不要碰。5.3 多架构镜像与 buildx在 x86 上构建 ARM 镜像如果你要在 ARM 边缘设备上部署推理服务比如 RK3588、Jetson 这类开发板那么镜像架构的问题就绕不过去了。在 x86 服务器上随手构建的镜像是linux/amd64拿到 ARM 机器上直接报exec format error原因就是 ELF 文件格式不匹配。现在主流做法是使用 BuildKit 的多架构构建能力。具体命令大致如下docker buildx create --name my-builder --driver docker-container --bootstrap docker buildx build --platform linux/amd64,linux/arm64 \ -t registry.example.com/inference-api:v2.1.0 \ --push .--platform可以同时指定多个目标架构。BuildKit 会利用模拟器qemu或者在远程构建机上执行不同架构的构建步骤最后为每个架构生成一份镜像并打包成一个 manifest list。在目标机器上 pull 时Docker 会自动拉取与当前架构匹配的镜像。这里要特别提醒一个交叉构建的注意事项如果你的 pip 依赖包含需要现场编译的包x86 的编译产物在 ARM 环境下是无法直接安装的。多架构构建的意思并不是“一份文件到处跑”而是“每个架构各构建一份”只是 Dcoker 的 manifest 机制让使用方无感。构建这类镜像时要格外留意每个架构下依赖包的实际兼容性不能想当然。6. 部署中最常见的故障与排查思路6.1 容器启动后立即退出怎么定位容器部署后最让人崩溃的场景之一就是启动后立刻退出。第一步不是看代码而是执行docker ps -a找到容器的退出码然后docker logs --tail 100 容器名看报错。退出码是快速定位问题的钥匙我整理了一张速查表退出码常见原因排查方向0前台进程主动退出命令正常结束启动命令是否正确是否进程在后台运行1应用逻辑抛错看日志检查配置、端口、依赖126命令存在但没有执行权限检查可执行文件权限、挂载目录是否 noexec127命令不存在检查镜像内是否缺少依赖shell 命令是否拼错137进程被 SIGKILL内存超限检查 --memory 限制和日志139进程段错误多为架构不匹配、依赖库版本冲突结合我自己排查的经验我有几条比较固定的顺序。先看日志日志里如果是“文件找不到”优先怀疑 WORKDIR 不对、代码没 COPY 全、环境变量没传递。如果日志什么都没有那就是启动命令执行后立刻正常退出这时候用docker run --rm -it --entrypoint sh 镜像名手动进入容器启动服务和看进程状态往往比看日志更快。如果是 137基本就是内存用量超过--memory限制了。检查系统日志dmesg确认 OOM killer 是否杀过容器进程然后把资源上限调高或者优化应用内存占用。6.2 构建阶段经常踩的坑构建 COPY 报错找不到文件大概率是.dockerignore误伤了源文件或者构建时指定的上下文目录不对。你先确认 docker build 命令的最后一个参数目录再检查.dockerignore里的规则是否把目录排除了。构建过程非常慢经常卡在 pip/apt 下载一方面是网络问题另一方面是你没有重复利用缓存。先检查是不是每次构建都把顺序搞反了导致依赖层缓存失效再检查 pip 是否设置了PIP_NO_CACHE_DIR1避免把大量下载缓存留在镜像里。构建时报 exec format error镜像架构和当前 Docker 主机不匹配。你是在 x86 上构建 arm64 的镜像但没启用 buildx 模拟器或者依赖包的架构不兼容。构建成功但运行时缺动态库这是最容易被忽略的。基础镜像换成 slim 之后很多动态库被裁剪掉了你在构建阶段编译正常但运行阶段找不到 libxxx.so。解决思路就是我在第 3 部分说的把运行阶段用到的动态库显式安装进去比如 libgomp1并在启动验证时用ldd检查可执行文件的动态链接。6.3 容器网络问题快速定位容器网络问题的排查思路和裸机网络调试有一点差别但也有固定的套路。首先确认容器用的是哪个网络模式。默认的 bridge 网络下容器有一个独立的网段宿主机和容器之间通过端口映射通信。如果你发现-p映射了端口但从外界访问不到先把docker ps里 Ports 列的格式看清楚如果是127.0.0.1:8000-8000/tcp说明只绑了回环地址外部访问当然不通。这种情况要么用宿主机的反向代理转发要么把映射地址改成对外网卡。多个容器之间互相访问不通优先检查它们是否在同一个网络里。docker network inspect 网络名可以看到网络包含哪些容器。compose 里创建的服务默认在一个自定义网络里但用docker run单独起的容器默认在 bridge 网络两者之间默认不通。关于 DNS 问题容器内的/etc/resolv.conf默认继承宿主配置。如果容器内能 ping 通 IP 但解析不了域名那就是 DNS 配置不对。可以在docker run里加--dns 8.8.8.8或--dns 公司内部DNS地址覆盖。这里千万注意不要一遇到容器网络问题就开始怀疑 Docker先分别验证宿主机的网络连通性、容器内用 IP 直连是否成功、再到 DNS 层定位一层层推进才是高效的排查方式。说到最后分享一个我自己确实受益很大的习惯。每次构建完成不管时间多紧我都会先顺手执行docker run --rm -it --entrypoint sh 镜像名进去把启动命令、工作目录、当前用户、环境变量、关键文件权限都验证一遍确认无误了才 push。这个动作看着土但能提前拦住至少一半的部署事故。另一个容易被低估的习惯是给每个镜像打上不可变的版本号绝不使用latest。镜像不是运行时的黑盒而是团队构建的交付产物把镜像的生成、验证、发布链路理清楚比硬背几百条 docker 命令有用得多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →