开头最早接触Docker那会儿还是在一次服务器部署的连续翻车现场。一个应用要从开发环境挪到测试环境再从测试环境挪到生产环境依赖装一遍、配置调一遍、权限踩一遍每次都是三五个小时起步。后来某位前辈丢了一句话“所有环境的差异本质上都是版本的差异和依赖的差异Docker就是把这俩差异吃掉的技术。”到现在这句话依然是我向新人解释Docker的第一句开场白。Docker的核心关键词就三个镜像、容器、仓库。镜像是一个打包好的、只读的运行环境模板容器是这个模板跑起来之后形成的一个独立进程环境仓库则是存放和分发镜像的地方。你可能会问这东西和虚拟机有什么区别最简单的理解是虚拟机模拟了一整台电脑的硬件所以它笨重、启动慢Docker只隔离了运行的进程和文件系统共享宿主机的内核所以它轻量、启动快一秒内就能起一个环境。这篇文章是“Docker全解析”系列的第一篇适合刚接触容器技术、准备把应用容器化部署的开发者也适合那些听说过Docker但始终没搞明白它到底解决什么问题的人。我会从环境安装讲起把镜像、容器、网络、数据卷、Compose编排这些基础内容逐个拆开最后附上实操中常见的坑和排查思路。全程按实际踩过的经验来讲不走教科书路线看完你能动手把一套应用跑起来并且知道为什么这么跑。1. Docker是什么先搞懂它到底解决了什么问题1.1 环境不一致的痛苦容器是怎么消除的在没有Docker之前部署一个Web应用的标准流程大概是这样的把代码打包传到服务器装Python或者Node或者Java环境装数据库客户端配环境变量修依赖冲突再手动启动服务。这中间每一步都可能因为系统版本不同、预装软件不同、库版本冲突而挂掉。最让人头疼的是开发环境明明正常一到测试环境就崩问题最后查出来往往是系统自带的某个库版本跟代码依赖的版本不兼容。Docker的思路完全不同。它把应用本身连同它需要的所有依赖、配置、系统文件全部打进一个叫“镜像”的静态文件里。开发环境用这个镜像跑测试环境用这个镜像跑生产环境也用这个镜像跑。因为镜像里的内容是固定的所以跑出来的行为基本一致。这就像一个外卖包装盒后厨用什么样的锅碗瓢盆炒出来的菜包装好送出去之后你打开吃到嘴里的时候它依然是那个味道不存在你家的灶台火力跟后厨不一样导致菜糊了的情况——因为给你的是成品不是半成品。这种一致性带来的直接好处是从“部署一次靠运气”变成“部署一次以后每次都能复现”。我见过很多团队引入Docker之后新人上手的效率明显提升因为不需要再写一份冗长的环境搭建文档直接一条命令就把环境拉起来所有环境变量和服务依赖都提前封装在镜像里。1.2 容器和虚拟机一个共同点三个关键差异虚拟机通过虚拟化硬件的方式在宿主机上跑一个完整的操作系统里面再装应用。Docker容器则是直接运行在宿主机内核之上的用户态进程它通过Linux内核的Namespace做资源视图隔离通过Cgroups做资源配额限制。两者有三个关键差异值得你记下来。体积层面一个CentOS虚拟机镜像动辄几GB一个CentOS容器基础镜像只有一两百MB。启动速度层面虚拟机启动像开机要经过BIOS、引导加载器、内核初始化几十秒到几分钟容器启动就是拉起一组进程毫秒级到秒级。性能损耗层面虚拟机有Hypervisor层做指令翻译和设备模拟多少会有性能损耗容器直接调用宿主机内核的系统调用性能接近原生。不过容器也有它的边界。由于共享宿主机内核容器里跑不了与宿主机内核不兼容的程序比如你不能在Linux宿主机上用容器跑一个Windows内核依赖的应用。而虚拟机因为模拟了完整硬件理论上什么系统都能装。这个差异决定了选型的逻辑追求轻量和高效用容器追求强隔离和跨内核跑虚拟机。大多数互联网后台服务、微服务应用场景容器完全够用。1.3 Docker的三大核心组件到底怎么配合Docker的体系里有三个组件分别是客户端、守护进程和仓库。客户端就是你敲的docker命令它通过socket或者HTTP接口把指令发给守护进程守护进程负责管理镜像、容器、网络和数据卷等对象。当你要拉取一个镜像时守护进程会去Docker Hub或者其他仓库下载下载完存到本地镜像缓存里。用一张图在脑子里模拟这个过程docker pull命令下发 - 守护进程向镜像仓库发起下载 - 镜像分层存储到本地 - 后续docker run基于这些镜像层启动容器。镜像本身是只读的容器运行过程中如果有文件新增或者修改会发生在容器自己的可写层里不会改动原始镜像。这也是Docker支持一个镜像同时启动多个互不干扰容器的底层原理。这里可以展开一个常见的面试问题数据卷和容器可写层的区别。容器可写层跟随容器生命周期容器删除后就没了数据卷是挂载在宿主机上的目录或独立存储单元容器删了卷还在。搞清楚这个边界后面讲数据持久化的时候你就不容易绕晕。2. 环境准备安装Docker并跑通第一个容器2.1 不同系统下的安装方式与选择建议Linux上装Docker最简单的方式是用官方脚本但实际工作里我更推荐通过发行版的软件仓库直接安装因为这样可以跟系统的包管理机制统一卸载和升级都方便。如果你是Ubuntu基本就是apt install docker.ioCentOS 7用yum install docker。安装完之后记得把当前用户加入docker组否则每次执行docker命令都要sudo。这一步经常会被人忽略导致第一次测试时明明装好了却总是报权限错误。Windows系统上装Docker现在都用Docker Desktop它底层依赖一个轻量级虚拟机跑Linux内核这种方式对桌面用户最友好。但要注意Docker Desktop的资源占用不算低老机器跑起来会有点吃力。如果你只是想临时用Docker跑个数据库或中间件不用Docker Desktop完全可以用WSL2Windows Subsystem for Linux配合命令行安装原生Docker性能和资源占用会更好。Mac上的Docker Desktop同样依赖虚拟化框架Apple Silicon芯片的机器一般都没问题但要注意给Docker Desktop分配的内存和CPU资源要留足如果虚拟机内存给太少构建大型镜像时容易出现内存不足的错误。提示不管哪个系统装完之后先执行docker version确认客户端和守护进程都能正常工作再执行docker run hello-world做一次全过程自检。这一步能帮你排除安装不完整、权限配置、底层虚拟化支持三类常见问题。2.2 镜像加速配置与第一容器实操在国内网络环境下直接访问Docker Hub拉取镜像经常会遇到速度慢或者超时这时候就需要配置镜像加速器。这个配置本身是写在Docker守护进程的配置文件里的Linux上路径通常是/etc/docker/daemon.jsonWindows和Mac的Docker Desktop则可以在设置界面里直接填加速器地址。配置完成后要重启Docker服务才能生效。加速器本质上是一个反向代理缓存它帮你把镜像从官方仓库先搬到一个国内访问速度更快的节点上。你配置好加速地址之后docker pull走的其实还是同样的镜像仓库但实际下载流量是从加速节点走的所以速度会明显提升。一个常见误区是只配置加速器却完全不检查是否生效建议配置完之后执行docker info在返回结果里找Registry Mirrors一栏确认。跑第一个容器的实操命令如下docker run -d --name nginx-demo -p 8080:80 nginx:alpine这条命令的含义是以后台方式运行一个叫nginx-demo的容器把宿主机的8080端口映射到容器内的80端口使用nginx的alpine精简版本镜像。跑完之后打开浏览器访问http://localhost:8080如果看到nginx的默认页面说明容器已经把服务正常跑起来了。这一步是整个Docker入门过程中最直观的一次正反馈。2.3 理解容器的生命周期从创建到销毁容器的生命周期管理是日常使用频率最高的操作。创建一个容器但不起动用docker create启动一个已存在的容器用docker start直接创建并启动用docker run。容器运行起来之后可以用docker ps查看正在运行的容器列表加-a参数能看到所有容器包括已经退出的。停止容器用docker stop它会先给容器内主进程发送终止信号等一段时间如果进程还没退出再强制结束。想立即终止用docker kill。删除容器用docker rm默认只能删除已停止的容器如果想强制删除正在运行的容器要加-f参数。另外还有docker pause/unpause用来暂停和恢复容器内所有进程这个操作在需要做快照或者短期冻结状态的场景下很有用。新手经常踩的坑是混淆docker stop和docker kill的区别以及不知道docker rm -f其实是stop和rm的组合操作。这些命令的语义差异在你做自动化脚本或容器编排时会直接影响服务的优雅下线能力所以一开始就养成准确的命令习惯很重要。3. 镜像管理拉取、构建、导出与分层原理3.1 镜像的分层存储机制Docker镜像并不是一个完整的单一文件而是由一层一层的只读文件系统拼接而成的。每一层都对应Dockerfile里的一条指令比如FROM创建基础层RUN执行命令创建新层COPY把文件加入镜像时也会创建新层。当你要拉取一个镜像时Docker会先检查本地已有的层只下载缺失的层这些层会被联合挂载为容器可用的根文件系统。这个设计带来的最大好处是空间复用。假设你本地有nginx镜像和一个基于相同基础镜像构建的应用镜像两个镜像大概率共享底层的基础层不必重复存储。最直观的验证方式是执行docker images看到不同镜像占用的总磁盘空间反而小于它们各自大小之和。理解分层机制后写Dockerfile时就要有意识地考虑层的数量和顺序因为每一层都会占用额外的构建时间和存储空间。还有个值得注意的点是镜像删除的细节。docker rmi删的是镜像的引用关系如果某个层被多个镜像引用Docker只会删除引用计数为零的层。有时候你执行完docker rmi发现磁盘空间似乎没释放就是因为同一层还被其他镜像占用着。3.2 Dockerfile基础与镜像构建示例构建自定义镜像是Docker实际使用中的核心技能。最原始的方式是启动一个容器在里面手动装环境、拷贝文件最后用docker commit把容器当前状态提交成镜像但这种做法不可复现、不透明所以现代实践全部围绕Dockerfile进行。Dockerfile就是一个描述“如何从零构建这个镜像”的脚本通过执行docker buildDocker守护进程逐行执行里面的指令并生成对应的镜像层。一个典型的Python Web应用Dockerfile长这样FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD [python, app.py]逐行解释一下。FROM指定基础镜像这是构建的起点WORKDIR设置工作目录后续相对路径的指令都会基于这个目录执行COPY requirements.txt先单独复制依赖清单是因为下一步RUN要安装依赖这样可以更好地利用构建缓存——如果requirements.txt没变Docker会直接复用缓存层不必重复执行耗时的pip install最后COPY把全部代码拷进去EXPOSE声明容器的监听端口CMD给出启动命令。3.3 多阶段构建一个技巧解决镜像体积痛点多阶段构建是在同一个Dockerfile里使用多个FROM指令每个FROM开启一个新的构建阶段最终产物可以只从最后一个阶段提取。这个技巧是解决编译型语言镜像体积问题的标准方案。举个例子你要把一个Go项目打包成镜像。如果直接在基础镜像里编译再运行基础镜像要包含整个Go工具链镜像体积可能达到800MB以上。用多阶段构建第一阶段用go镜像编译出二进制文件第二阶段从一个小的运行镜像开始只把编译好的二进制复制进来最终镜像可能只有二三十MB。FROM golang:1.21-alpine AS builder WORKDIR /src COPY . . RUN go build -o /out/app main.go FROM alpine:latest COPY --frombuilder /out/app /usr/local/bin/app ENTRYPOINT [app]这段Dockerfile里AS builder给第一阶段起了一个名字第二阶段通过COPY --frombuilder从该阶段取文件。最终镜像里既没有Go编译器也没有源码只有可执行的二进制和依赖的运行时库。在实际生产环境中我见过很多项目只引入这个方法部署镜像体积直接从GB级降到百MB级整个发布流程的速度肉眼可见地提升。3.4 镜像的导出、导入与私有化保存有些场景你需要在无法访问镜像仓库的机器上安装镜像比如内网服务器这时候需要把镜像导出成文件再拷贝过去。docker save可以将一个或多个镜像打包成一个tar压缩包docker load则可以把这些包重新导入。注意这两个命令和docker export/docker import的区别save导出的是镜像的完整分层信息适合镜像迁移和备份export导出的是容器的文件系统快照不保留镜像历史层信息和构建元数据。生产环境里更常见的做法是建立私有镜像仓库最轻量的方案是运行一个Registry容器。启动私有仓库后需要给本地镜像打上仓库地址前缀的标签然后才能push上去。整套流程看起来多了一个步骤但对于需要离线交付、合规审计和版本控制的团队来说私有仓库几乎是刚需。这里有一个容易踩的坑默认情况下私有仓库使用HTTP协议Docker客户端会拒绝推送报错提示是“server gave HTTP response to HTTPS client”。解决办法是在daemon.json里把该仓库地址加入insecure-registries列表或者在仓库端配置HTTPS证书。我刚开始搭私有仓库时卡了很久后来才明白这个安全策略的机制。4. 容器实操从运行参数到资源限制4.1 docker run的常用参数全景docker run是使用频率最高的命令但它的参数极其丰富很多参数组合起来才能支撑一个合理的生产环境运行方案。我从日常使用中挑几个最关键的参数展开讲。-p参数做端口映射格式是宿主机端口:容器端口。这里要注意容器内的端口是固定的宿主机端口可以自由选择凡是宿主机上被占用的端口都不能用。端口映射可以在启动容器时多次指定比如同时映射HTTP端口和调试端口。如果你需要宿主机随机分配一个端口来避免冲突可以使用-P参数它会将容器内所有EXPOSE的端口映射到宿主机的高位随机端口上配合docker ps查看实际映射关系。-e参数用来传入环境变量主要用于覆盖配置项或者传递密钥。例如启动MySQL容器时通过-e MYSQL_ROOT_PASSWORDxxx来指定管理员密码。配置项和密钥是否用环境变量传还是支持从配置文件读取取决于镜像本身的设计但容器化的通用原则是镜像里不写死敏感信息所有可变配置由环境变量或挂载文件提供。--restart参数决定容器退出后的处理策略。no是不重启on-failure表示非正常退出时重启可接重启次数always表示无论什么原因退出都自动重启unless-stopped表示除非手动停止否则永远重启。生产环境里除了那些必须手动控制的批处理容器大多数服务型容器都建议设成--restartunless-stopped这样宿主机重启后容器也能自动恢复。4.2 进入容器操作exec和attach的区别需要进入一个正在运行的容器内部排查问题时有两种方式。docker exec是主流方式它会在运行中的容器内再启动一个新进程比如docker exec -it nginx-demo /bin/sh会给你一个交互式shell。这个命令可以在不打断容器主进程的前提下执行任意命令非常适合定位问题。docker attach则是把当前终端的输入输出直接连接到容器主进程的标准输入输出上看起来也能进容器但风险在于如果你在attach状态下不小心关闭了终端容器的输入输出挂断主进程可能收到SIGHUP信号导致退出。所以日常调试场景我基本只用docker execattach已经很少碰了。使用exec时还有两个细节值得留意。第一很多官方镜像为了精简体积默认只装基本的shell可能是/bin/sh而不是bash所以提示command not found时先确认shell是否存在。第二容器内默认用户往往是root直接进入后操作文件系统时要小心不要随手改权限和属主否则可能造成意想不到的鉴权问题。4.3 给容器设置合理的资源配额容器虽然共享宿主机内核但CPU和内存的配额是可以限制的。默认情况下一个容器可以消耗宿主机的全部资源这在容器数量少的时候无所谓但也存在一个容器把宿主机资源耗尽拖垮其他服务的风险。生产环境的规范化操作是给每个容器设置明确的资源上限。内存限制用-m参数比如-m 512m表示容器最多使用512MB内存。超出限制时内核会触发OOM内存耗尽机制直接杀掉容器内最耗内存的进程所以内存限制的值要留足业务高峰的余量。CPU限制相对复杂一点--cpus1.5表示最多使用1.5个CPU核心的能力--cpu-shares则是权重分配。观察资源使用情况的命令是docker stats它动态输出所有容器的CPU、内存、网络和磁盘I/O使用量。我之前排查过一个偶发性卡顿问题就是用docker stats发现某个容器内存占用逐渐涨到限制值附近最终确认是代码里的缓存对象没有清理机制。4.4 容器日志的最后防线log驱动与查看方式查看容器日志用docker logs这是排查问题的第一战场。常见参数包括-f持续跟踪最新日志--tail 100只显示最后100行--since和--until按时间过滤。默认情况下docker logs收集的是容器主进程的标准输出和标准错误所以应用要想着把日志输出到标准输出流否则在容器外就是查不到。如果应用本身写日志文件而不打标准输出你当然可以用exec进容器看文件但容器一重建日志文件就跟着没了。合理做法是让应用把日志打到标准输出用Docker的json-file日志驱动统一收集需要持久化时再对接系统级日志收集方案。还有一个容易忽略的参数是--log-opt max-size和--log-opt max-file用于限制单个日志文件的大小和数量避免日志无限增长把宿主机的磁盘空间占满。我见过不止一次生产事故是容器日志把磁盘写满导致同节点上其他服务全部卡住这个配置其实一行就能防住。5. 数据持久化数据卷与挂载的取舍5.1 容器文件的易失性决定了持久化的必要性容器文件系统分为两层底层的镜像只读层和顶层的容器可写层。你运行容器时对文件系统的任何修改都发生在可写层里。当你执行docker rm删除容器时这个可写层连同里面的所有数据会一起消失。这就是容器数据易失性的根源。这也是很多新人刚上手时会犯的错误把数据库的数据文件直接放在容器默认路径里跑了很多天都很正常某次升级或迁移时容器被重建数据全没了只能从备份恢复甚至面对永久丢失的后果。要避免这种事故唯一的方案是让业务数据不依赖容器的可写层而是落在宿主机文件系统或者外部存储上。5.2 bind mount与volume使用详解与环境现实取舍Docker提供两种主流的数据持久化方式。第一种是bind mount指定宿主机上的一个具体路径挂载到容器路径启动时用-v /宿主机路径:/容器路径 或者更明确的--mount typebind,source宿主机路径,target容器路径。这种方式直观适合把配置文件、静态资源目录直接暴露给容器因为修改宿主机文件就等于修改容器文件。第二种是volume由Docker自己管理的一个目录卷通过docker volume create创建。volume的好处是路径不需要你手动指定Docker将它存放在自家管理的目录下同时在多个容器间共享数据时也方便。用-v 卷名:/容器路径 即可。两种方式的选择逻辑并不复杂。需要把宿主机已有的配置文件或代码目录直接映射进容器用bind mount需要存放数据库文件、上传目录等应用数据且无须关心具体路径用volume。有一个实际经验是bind mount的权限问题常常让人头疼因为宿主机目录的所有者和容器内进程的用户可能不一致要么在挂载时保证UID一致要么在运行容器时指定用户与宿主机匹配。5.3 数据卷容器与容器协作中的数据共享多容器之间共享数据除了用挂载宿主机同一路径的方式还可以借助数据卷容器。所谓数据卷容器是一个专门用来声明卷的容器它本身不跑业务只是作为一个数据挂载的中转站。其他容器通过--volumes-from 数据卷容器名 继承其挂载的卷。这种方式在旧版本Docker里很流行因为当时的卷管理能力有限。到了今天直接用命名的volume在多容器间挂载已经可以解决绝大多数共享需求。数据卷容器的应用场景现在只剩一些特定边界情况比如需要统一由某个容器管理卷的挂载点和配置或者做跨容器迁移时的承接。理解它的原理但不必当作主力方案这是我给大多数团队的建议。6. 容器网络理解默认网络模式与端口通信6.1 三种默认网络模式的适用场景Docker安装完成后会自动创建三个网络bridge、host和none。bridge是默认模式所有没有显式指定网络的容器都会加入这个虚拟网桥容器间可以通过IP直接互通也可以加上--link参数完成旧式的DNS映射。host模式让容器直接使用宿主机的网络栈不再做端口转换所以容器的端口就是宿主机的端口性能损耗最小但隔离性也最弱。none模式则完全不给容器配置网络接口适用于离线任务或对网络有严格安全要求的场景。日常开发中最常用的是bridge模式。在同一台宿主机上跑多个容器时bridge网络内的容器可以通过相互的IP或容器名访问。这里要特别提醒一个容易犯的错bridge模式下容器的IP是Docker动态分配的如果依赖写死的IP通信容器一旦重建IP就会变服务就断了。解决方式是用容器名做服务发现这也是接下来要说的问题。6.2 自定义bridge网络与容器名互访为了解决容器IP变化带来的问题Docker提供了自定义bridge网络的功能。通过docker network create创建网络启动容器时--network指定网络名同一个自定义网络里的容器就能自动用容器名直接互相访问。这个机制的底层逻辑是Docker内置了DNS解析服务当容器启动时把自己的名称和IP注册到网络DNS中其他容器通过名字查询就能拿到最新IP。有了这个能力你就不需要关心容器的IP是多少只要服务名称不变网络拓扑就稳定。用之前创建的nginx容器和一个测试容器演示一下docker network create demo-net docker run -d --name app-server --network demo-net nginx:alpine docker run -it --rm --network demo-net alpine sh进入第二个容器的shell之后执行ping app-server就能看到网络是通的。这是本地调试多容器应用时最常用的操作熟悉后对后面讲Compose编排是很好的基础。6.3 端口映射的背后原理与调试技巧bridge网络下容器默认是“封闭”的外部访问不到它的端口。要让外部访问就要在启动时用-p做端口映射。端口映射的实现机制是Docker会在宿主机上建立一条iptables规则把访问宿主机指定端口的流量转发到容器的对应端口。排查端口映射问题时可以用docker port 容器名 查看实际的端口映射关系。如果是宿主机上的端口监听有问题需要确认防火墙规则是否允许访问。还有一个调试技巧是临时启动一个带有网络工具的镜像容器让它加入同一个网络来测试容器间的连通性测试完删除即可。这个方式不会污染生产容器非常实用。7. 容器编排入门用Compose定义多容器应用7.1 为什么一个复杂应用不再是单条docker run实际业务中一个应用往往不止一个容器。一个简单的Web服务至少包含应用容器和数据库容器复杂一点的可能还要加缓存、消息队列、日志收集等组件。如果每个组件都写一条docker run命令管理起来不仅繁琐而且一旦宿主机重启手动恢复所有容器的顺序和参数都是巨大的负担。Docker Compose就是为解决这个问题而生的。它通过一个YAML文件把多容器的定义、网络、数据卷、依赖关系集中管理起来再用docker compose up和docker compose down一键启停整个应用栈相当于把一组容器的生命周期管理从手工操作变成了描述性配置。7.2 编写docker-compose.yml的实用套路一个典型的docker-compose.yml包含几个关键顶级字段version、services、networks和volumes。services里每个键代表一个服务对应一个容器。下面是常用来部署一个Web服务加PostgreSQL的示例services: web: build: . ports: - 8000:8000 environment: - DB_HOSTdb depends_on: - db db: image: postgres:16-alpine environment: - POSTGRES_PASSWORDsecret volumes: - pgdata:/var/lib/postgresql/data volumes: pgdata:两个要点值得展开。第一web服务里用build: .声明通过Dockerfile构建而db服务用image: postgres:16-alpine直接拉取现成镜像Compose允许同一个栈里混合构建镜像和现成镜像。第二environment里的DB_HOSTdb用的是db这个服务名这得益于Compose默认会给每个服务创建一个自定义网络服务名直接作为DNS解析跟上一章的自定义网络原理一致。7.3 启动、日志、恢复和更新的实际控制命令用Compose管应用栈最常用的命令是docker compose up -d它会在后台启动整个栈docker compose ps查看状态docker compose logs -f持续跟踪所有容器的日志docker compose down停止并删除容器但不会删除声明为volumes的数据卷。变更代码后想重新构建镜像并更新容器执行docker compose up -d --build即可。这个命令会先检查哪些服务需要重新构建构建完再滚动替换容器。还有一个实用参数是--scale可以快速把一个服务扩展到多个副本比如docker compose up -d --scale web3。不过横向扩容不是Compose的强项真正的大规模副本管理和弹性伸缩还是要交给容器编排平台Compose更适合中小项目和本地开发。8. 常见问题排查与避坑手册8.1 容器启动后立即退出的排查路径这是新手遇到最多的现象docker run之后容器秒退docker ps什么都看不到。第一步先看退出码执行docker ps -a找到这个容器的状态列。退出码为0说明主进程正常结束通常是启动命令写错了比如CMD指定的程序不存在或者直接执行完就没存活进程退出码非0说明程序出错用docker logs 容器名查看程序报错信息。还有一个很关键的原则容器必须有一个前台运行的进程。很多人习惯在Dockerfile里把启动命令写成service nginx start这类后台启动方式结果容器一启动就退出因为主进程跑完命令后就结束了。正确做法是使用NGINX这种镜像自带的启动命令它会在前台启动进程。这个细节每个踩过的人都会印象深刻。8.2 镜像拉取慢、超时与加速配置失效镜像拉取慢在国内是老话题了。除了配加速器之外还可以留意几个操作细节。一是尽量使用精简版本的基础镜像比如alpine或slim标签体积能小很多。二是分时间段拉取避开网络高峰。三是对于非常关键的镜像可以考虑提前拉取好再save成本地文件分发到目标机器。加速配置不生效时建议按顺序排查确认配置文件路径正确确认daemon.json是合法JSON格式确认重启了守护进程而不是只重启了客户端再docker info看Registry Mirrors字段。有一种情况是你明明配了多个加速地址但配置文件中写法不对导致整个文件解析失败Docker静默忽略掉了所以排查时先看docker info才是最直接的。8.3 容器内时间不正确与locale问题很多应用对日志时间有要求而容器默认的时区是UTC比北京时间慢8小时。解决方案是在启动容器时挂载宿主机的时区文件例如Linux下用-v /etc/localtime:/etc/localtime:ro或者在Dockerfile里设置环境变量TZAsia/Shanghai。需要注意挂载host的localtime文件方式在部分发行版上因为文件变化机制而失效此时用环境变量TZ更可靠。locale问题主要影响一些依赖系统区域设置的应用比如Java应用的打印中文日志时出现乱码。在运行的容器里执行locale命令查看已安装的语言包一般基础镜像不会安装完整的locale数据可以在Dockerfile里加上相应的设置命令或者尽量让应用不依赖系统的locale判断。8.4 磁盘空间被镜像、容器和日志逐步吃光Docker用久了磁盘空间会被三类东西占满无用的镜像、停止的容器和增长中的日志文件。定期执行docker system df可以查看当前的占用分布。清理无用镜像用docker image prune清理所有已停止容器用docker container prune。更彻底一点docker system prune -a会把当前未运行的容器、未使用镜像、未使用网络和构建缓存一并清掉但要注意它不会清理正在使用的数据卷。针对日志增长最有效的前置方案是启动容器时加上日志大小限制这个前面在日志部分详细提过。还有一点容易忽视的是构建缓存反复构建同一个项目会让构建缓存占掉大量磁盘。加上管理策略定期清理构建缓存是生产环境必须纳入巡检的环节。结尾我个人在实际操作中的体会是Docker的难点从来不在命令本身而在于你得先建立一套“运行时环境可描述、可共享、可销毁”的思维方式。镜像就是不可变的交付物容器就是不断被创建和销毁的执行单元数据卷才是真正需要长期留存的东西。想明白这三者的边界后续再学网络、存储和编排都会顺手很多。最后再分享一个小技巧刚开始用的几天建议每敲一条不熟悉的命令前都先用docker --help或者docker 子命令 --help看一下完整参数列表。Docker的文档风格比较平实参数名基本都能看出用途养成查帮助的习惯比背几百个参数组合要高效得多。这个系列下一篇会进一步拆解更高级的容器网络隔离、镜像安全扫描和服务编排实践到时候可以继续沿着这条路往下走。