Docker基础学习笔记:从镜像容器到常用命令与实战踩坑经验
1. 为什么我最终把Docker放进了日常工具箱1.1 从一次环境翻车说起最早接触Docker我其实有点不以为然。那会儿我总觉得部署不就是一个“代码拷过去依赖装一遍服务跑起来”的事情吗直到有一次某个后端同学把一套开发好的服务扔给我说“你本地跑一下看看”。我在自己机器上折腾了一下午先是对不上依赖版本装完一个包又提示另一个库缺失好不容易编译通过运行时报错信息直接指向系统库版本过旧。那一刻我真的想把电脑砸了。后来那位同学丢过来一条命令大概就是先拉一个镜像再docker run一把前后不到五分钟服务就正常跑起来了。也就是从那天起我决定静下心来把Docker的基础彻底啃一遍。这份学习笔记没有多么高深的理论更多是我从零到能独立使用的过程记录Docker到底解决了什么问题镜像、容器、仓库之间是什么关系安装之后有哪些容易踩的坑日常命令背后到底发生了什么以及怎么用Dockerfile把环境固化下来。如果你也是刚接触Docker、被一堆概念绕晕的人这篇内容应该能帮你把最基础的知识框架搭起来。1.2 Docker解决的核心问题环境一致性Docker最核心的价值不是“把服务封在一个盒子里”这么简单而是环境一致性。传统的方式交付的是“代码 安装文档”但文档写得再详细也架不住每个人机器上的操作系统版本、系统库、运行时环境、目录结构都不一样。Docker的思路是把运行环境本身也变成交付物把代码、依赖、运行时、配置文件全部打包成一个独立的镜像文件拿到任何一台机器上都是同一套环境跑同一个东西。我以前总觉得“在我机器上是好的”这句话很可笑但有了Docker之后这句话可以真正变成“在我机器上是好的在你这台也一样”。镜像构建完成后运行结果和构建时几乎完全一致不再需要反复纠结“为什么你那边缺库”“为什么他那边版本不对”。这种确定性在开发和运维协作中极其有价值。1.3 它和虚拟机有什么本质区别很多初学者会把Docker和虚拟机混为一谈这其实是不太准确的。虚拟机是在宿主机上虚拟出一整套硬件然后在里面跑一个完整的操作系统所以资源占用高、启动慢。Docker不是虚拟硬件而是共享宿主机内核在操作系统层面做隔离每个容器只是宿主机上的一组进程有自己的文件系统视图和网络栈再加上一层隔离机制。理解它们之间的区别我习惯用一个不太严谨但很好懂的类比虚拟机就像你把整个家当都搬走搬到哪都得先把房子盖好Docker则像标准集装箱货物装好箱之后不管哪个港口有合适的吊机都能快速装卸。虚拟机动辄几十秒甚至几分钟启动容器通常是毫秒到秒级虚拟机独占大量内存和磁盘容器跑在同一套内核之上资源密度高得多。1.4 这份笔记适合谁这份笔记主要适合两类人。一类是完全没接触过Docker只听过名字但不知道它解决什么问题的新手另一类是已经敲过一些命令但只是照着文档复制粘贴遇到问题就懵的开发者。我不会去抄官方文档里面每个参数的解释而是把“为什么要这样操作”和“操作后发生了什么”讲清楚。学完这套基础之后再去看官方文档或者更深入的网络、存储、编排内容会轻松得多。2. 先把三个核心概念揉碎了再动手镜像、容器、仓库2.1 镜像只读的环境快照镜像是一个只读的模板里面装着运行某个应用所需的全部东西操作系统基础文件、运行时、库、代码、环境变量、默认配置。镜像是分层构建的每一层都代表文件系统的一次变更多个层叠在一起变成一个完整的只读快照。层可以复用这也是为什么Docker能在多项目、多环境之间省下大量磁盘空间。我理解镜像用得最多的类比是“底片”。底片本身不能直接用来播放但它是所有照片的模板。你把环境和应用一起“冲印”成底片之后任意一份复制品都能洗出一样的照片。修改照片内容不会改变底片除非重新制作新的底片也就是重新构建镜像。2.2 容器镜像运行起来的实例容器是镜像运行之后的实体本质上是一个或一组进程拥有独立的文件系统、网络接口、进程视图。同一个镜像可以创建多个容器容器之间互相隔离。容器可以启动、停止、删除也可以进入容器内部执行命令甚至可以在容器里安装临时软件。但容器生命周期内做的任何修改默认情况下不会回写到镜像里除非你专门用命令把容器提交成新镜像。一句话总结镜像是模板容器是模板跑出来的实例。你做实验时随便折腾容器就好折腾坏了删掉再起一个镜像还在成本非常低。2.3 仓库镜像的存放与分发中心仓库是集中存放和管理镜像的地方。平时执行拉取、推送镜像的操作都是和仓库打交道。仓库分公开和私有两种公开仓库适合分享通用镜像私有仓库适合存放公司内部或个人的项目镜像。镜像的完整名字通常由“仓库地址 / 命名空间 / 仓库名 : 标签”组成标签用来区分同一镜像的不同版本。如果你不写标签默认会带上latest标签。这个习惯在学习和测试时没问题但在正式场景里我建议尽量指定明确版本号因为latest是动态的过段时间再拉可能就不是同一个内容了。2.4 三个概念之间的关系把三者放在一起看会更清楚概念本质类比常用操作镜像只读模板由多层文件系统组成底片 / 安装包构建、拉取、推送、删除容器镜像的运行实例可写可启可停照片 / 运行中的程序创建、启动、停止、进入、删除仓库镜像的集中存储与分发服务相册 / 软件源拉取、推送、搜索从使用路径来说通常是从仓库拉镜像用镜像创建容器在容器里运行服务需要变更环境时改镜像重新构建再重新生成容器。理解这条路径之后后续所有命令都会变得顺理成章。3. 安装与初始化文档不会告诉你的三个坑3.1 先选对安装方式安装Docker方式很多但大体分两类。一类是桌面版方案适合本地开发调试自带图形管理界面底层会帮你维护一个轻量的虚拟机环境另一类是纯命令行引擎一般装在Linux服务器上资源占用更小没有图形界面只提供核心的容器运行能力。如果只是自己学习开着图形界面其实更方便观察容器状态如果以后要部署到服务器纯命令行引擎才是常态。安装过程本身不复杂跟着官方文档走基本不会出大问题。真正的坑都在安装完之后。我第一次装好之后满心欢喜地执行第一条命令结果直接被权限错误浇了一盆冷水。3.2 Linux下的权限问题在Linux环境下直接执行docker ps往往会看到关于权限的报错大致意思是当前用户没有权限访问守护进程的接口。原因是Docker守护进程默认以系统管理员权限运行它的通信端口只对管理员或docker用户组的成员开放。解决办法是把当前用户加入docker用户组然后重新登录会话这个用户组加入之后普通用户就能正常执行Docker命令了。这里我想多说一句加入docker用户组等于把这个用户的环境权限提得很高因为能操作Docker的人基本可以间接操作宿主机上的文件。所以开发机上图省事可以这么做但生产环境最好还是用更严格的方式管理访问权限不要随便把用户塞进这个组。很多人只记住了“加组”忽略了这背后的风险我是建议你至少心里有数。3.3 验证安装是否成功装好之后别急着跑业务先确认环境是不是真的健康。一般我会依次执行三条命令docker version docker info docker run --rm 一个测试镜像第一条看客户端和服务端的版本是否都能正常显示第二条看整体的运行环境信息比如容器数量、镜像数量、存储驱动等第三条会用最小的测试镜像验证“拉镜像、创建容器、运行、输出、退出”这条完整链路通不通。如果测试镜像能跑起来并正常退出说明安装基本没问题。如果卡在权限或者网络环节前两步会提前暴露问题。3.4 镜像加速源配置国内环境下直接拉官方仓库镜像速度经常让人崩溃。我一开始没在意拉一个体积稍大的镜像进度条半天不动最后还超时失败。后来在配置目录下新建或修改了一个配置文件把镜像加速地址写进去才算彻底解决。配置思路很简单在守护进程配置文件中加入registry-mirrors字段填写可用的加速地址然后重启守护进程。我当初配置文件大致是这个样子{ registry-mirrors: [https://某个镜像加速地址] }改完重启之后用docker info查看镜像配置部分能看到加速地址是否生效。加速源只是让拉取更快不会改变Docker本身的行为。配置好之后再拉镜像就顺畅多了。4. 核心命令背后真正发生的事4.1docker pull先想清楚标签再动手docker pull负责从仓库拉取镜像到本地。技术上它会和仓库通信比对本地已有的镜像层只下载缺失的层因此同一台机器第二次拉相同镜像会很快。这个命令真正需要留意的不是命令本身而是标签。如果直接写docker pull 某个镜像:latest拉下来的是最新版本但你的项目下次构建可能就会因为基础镜像更新而莫名其妙出问题。我现在的习惯是明确需要某个版本就写具体标签不需要最新版本就不要碰latest。这不只是稳定性问题还关系到可复现性——同一个标签在时间上是不确定的。4.2docker run参数拆分docker run是把镜像变成容器的最常用命令。第一次看它的时候那一长串参数确实吓人但其实拆开就很好懂。比如docker run -d -p 8080:80 --name web-test 某个镜像名称:版本号-d表示后台运行终端里不会挂着日志容器在后台持续运行--name给容器起个名字之后操作不用再查容器ID-p 8080:80是端口映射宿主机上的8080端口转发到容器内的80端口。这里有一个容易忽略的点容器内部有自己独立的网络命名空间默认情况下宿主机访问不到容器里的端口。-p会在宿主机网络层加一条转发规则把宿主机端口流量导入容器端口。为什么要把端口映射成“宿主机端口:容器端口”而不是只写一个端口因为同一个容器内端口是固定的宿主机端口却可能被其他服务占用。比如容器内服务跑在80端口但宿主机80端口已经用于别的东西了这时就可以把宿主机的8080映射到容器的80外部通过8080访问服务。4.3 看状态和日志docker ps与docker logs容器启动之后要用docker ps查看正在运行的容器列表看到的是精简输出容器ID、镜像、命令、创建时间、状态、端口映射等。docker ps -a会额外显示已退出的容器。关于容器状态我整理了一张表状态含义常见原因Up容器正在运行服务常驻后台或带交互进程Exited容器已退出主进程结束、启动失败、主动停止Paused容器暂停运行执行暂停命令后Restarting正在重启配置了自动重启且进程异常退出docker logs -f用来查看容器日志-f是跟随输出类似tail -f。排查问题时我通常带--tail 100只看最近100行避免刷屏。容器日志和普通服务日志的区别在于Docker默认会把容器内主进程的标准输出和标准错误收集起来所以应用只要把日志写到标准输出就能统一从docker logs看到。4.4 进入容器能不进就不进有时候需要进到容器内部看看环境、检查文件这时执行docker exec -it 容器名 shexec是在运行中的容器里额外执行一个进程-i保持标准输入打开-t分配一个伪终端两个参数配合在一起才能让你得到一个可以交互的命令行。很多基础镜像里没有完整的bash只有一个精简的sh所以进入容器优先试sh不行再换bash。但我要真诚地说一句进入容器手动改动是应急手段不是常规操作。容器本质是“以镜像为模板生成的临时运行环境”里面任何手工改动都会在容器被删除后彻底消失。如果发现容器里缺了什么、配置要改正确做法是去更新Dockerfile、重新构建镜像再起一个新容器。你如果总在容器里敲命令改东西说明构建流程还没有固化下来。4.5 停止、删除和清理停止容器用docker stop 容器名删除容器用docker rm 容器名。删除镜像用docker rmi 镜像名。需要注意删除镜像前得先确认没有正在运行的容器还引用着它。清理类命令是双刃剑。docker system prune会清理所有已停止的容器、无用网络、悬空镜像等加-a会把所有未被使用的镜像也一并删掉。我建议先了解清楚再执行-a不然本地留着的一堆基础镜像可能需要重新下载。实际操作中我更习惯按需手动清理而不是动不动就全量清空。5. 编写Dockerfile把环境固化成一串可复制的文本5.1 一个最小可用的示例日常使用Docker肯定会遇到需要把自有服务或自定义环境做成镜像的情况。Dockerfile就是描述“镜像如何构建”的文本文件。下面是一个最小可用的示例FROM 基础镜像名称:版本号 WORKDIR /app COPY . /app RUN 执行一些安装和配置命令 ENV 某个环境变量值 EXPOSE 8080 CMD [启动命令]每一行都干了一件事FROM指定基础镜像这是新镜像的地基通常选一个带系统环境和运行时的镜像WORKDIR切换工作目录后续命令都在这个目录里执行COPY把本机文件复制进镜像RUN在构建过程中执行命令安装依赖、改配置都靠它ENV设置环境变量EXPOSE只是声明容器内服务使用的端口它是一个文档性声明并不会真的自动映射端口CMD定义容器启动时默认执行的命令。初学阶段不要贪多先把这几条指令用熟后面需要再查其他指令。我当初就是被一堆指令名吓住后来发现日常工作里高频出现的也就那么十来个。5.2 指令顺序为什么影响构建速度Docker构建镜像时每一条指令都会生成一个只读层。每一层如果内容和上次构建完全一致就可以直接复用缓存跳过执行。这就是为什么调整Dockerfile里的指令顺序会让构建速度差别巨大。正确的策略是“把不常变的放前面常变的放后面”。比如先FROM再RUN安装依赖然后才COPY项目代码。这样只要依赖没变前面这些层都能命中缓存如果反过来先把代码COPY进去再执行安装那么代码每次变动都会让后续所有层缓存失效等于每次全量重来。5.3 构建上下文最后一个点不是开玩笑执行docker build -t 镜像名:标签 .的时候最后一个点容易被忽略。它不是指当前目录而是构建上下文。Docker会把指定目录下的所有文件打包发送给守护进程供Dockerfile里的COPY等指令使用。我见过有人直接在项目根目录执行构建结果项目里塞着几个G的依赖目录和缓存文件构建过程卡到怀疑人生。解决方法是尽量让上下文目录精简并且学会用.dockerignore文件排除不需要的文件。这个文件的作用和.gitignore类似在里面写要排除的路径构建时这些文件就不会被发送过去。另外COPY只认上下文里的文件别想着从上下文之外复制东西进去那不是Docker的设计方式。5.4 一个容器只跑一个主进程写Dockerfile时最容易犯的错误是在里面安装好多服务然后试图让容器启动时把多个服务一起拉起来。容器不是完整的操作系统使用方式它只有一个主进程作为PID 1容器的生命周期和这个主进程绑定主进程退出容器就停止。所以一个容器最好只承担一个职责跑一个主进程。如果确实需要多个服务协作正确的做法是用多个容器通过Docker网络互相通信再借助外部编排能力统一管理。刚开始学习时建立“单容器单职责”的意识后续编排会省心很多。6. 数据卷与网络容器删了数据还在吗6.1 默认情况下容器删除数据就没了容器运行时产生的文件默认写在一个临时可写层里。这个可写层和容器绑定容器一删数据跟着消失。如果你在里面跑个数据库容器没了数据全丢这种损失是没法接受的。我在初期其实吃过这个亏。当时启动了一个带数据库的容器往里灌了不少测试数据后来为了清环境顺手把容器删了数据也一起没了。从那之后凡是有状态的服务我都会在启动前想好“数据放哪里”。6.2 数据卷的两种常用方式解决数据持久化最常用的有两种方式。一种是目录挂载直接把宿主机的一个目录映射到容器内目录docker run -v /宿主机目录:/容器内目录 镜像名比如把宿主机的配置目录挂进容器容器里写文件会直接落到宿主机目录上。这种方式非常方便调试改一下宿主机文件容器里立刻能看到。另一种是卷volume由Docker自己管理存储位置推荐用于需要长期保存的数据因为卷的备份和迁移更规范不会散落在宿主机的各个角落。用卷的方式可以先创建命名卷也可以启动时直接挂载。比如创建一个名字叫app-data的卷然后运行容器时挂到容器内指定目录后续不管容器怎么删、怎么重建只要挂载卷的名字不变数据都在。6.3 网络模式到底是怎么回事Docker默认为容器提供隔离网络环境常见模式有三种。bridge模式会让容器通过一个虚拟网桥连接宿主机是默认模式也是端口映射的基础host模式直接复用宿主机网络命名空间容器不需要端口映射直接使用宿主机端口但也不好控制端口冲突none模式则完全不配置网络适合对网络有特殊要求的场景。端口映射本质上是在默认的桥接网络里加了一条转发规则把宿主机端口收到的流量转给指定容器的端口。你看到的-p参数就是干这件事。如果你只设置了容器内的端口而没有做映射外部是访问不到的因为容器有自己独立的网络空间。6.4 多容器之间怎么互相访问同一个项目可能有应用容器和数据库容器它们之间需要通信。很多人下意识会去查容器的IP地址然后写固定IP但容器IP在每次重建后都可能变化这种方案很不稳。更好的做法是创建一个自定义网络然后把需要通信的容器都加入这个网络。在自定义网络里容器ID或容器名可以被解析成容器IP也就是说应用容器里连接数据库填写的主机名可以直接是数据库容器的名字。只要容器加入同一个网络互相访问就用容器名作为主机名IP怎么变都不影响。这个特性是真正常用的而且比固定IP靠谱太多。7. 我踩过的坑从排查中积累的实战经验7.1 容器时间差8小时日志全错位我最初跑容器时发现日志里的时间和宿主机对不上信息查了一圈才发现很多基础镜像默认时区是UTC也就是零时区而本地是东八区天然差8个小时。排查问题的时候带错位时间看日志越看越乱。解决办法是在Dockerfile构建阶段处理而不是每次启动的时候处理。通常先安装时区数据包然后设置时区环境变量再把它设置到环境里。这样构建出来的镜像自带正确时区任何地方启动都对。如果不想改镜像也可以在运行容器时挂载宿主机的时区文件但这种方法在有些基础镜像上不生效不如构建时处理好。7.2 容器里出现“收不走的”子进程跑容器时如果里面还启动了子进程主进程退出后子进程可能变成孤立的且没人回收导致容器里积攒一些残留进程。这个问题在有多个进程的容器里尤其明显。后来我了解到轻量级的初始化进程方案可以让一个极小的进程充当容器的PID 1由它负责转发信号、回收子进程再让真实业务进程作为它的子进程运行。这就像给进程安排了一个“管家”专门处理前后台关系。第一次接触这个概念觉得不以为然直到排查过一次诡异的重启问题才意识到PID 1的信号处理行为有多么关键。7.3 日志无限增长磁盘悄悄满了默认情况下Docker把容器日志收集到宿主机如果应用打印日志比较频繁文件会越来越大直到把磁盘塞满。排查的时候往往只盯着应用层没想到是容器日志文件在背后猛涨。现在我养成了两个习惯。一个是启动容器时给日志轮转加限制比如限制单个日志文件大小和保留数量另一个是在全局配置里把日志参数作为默认值这样就算有人忘了配也不至于失控。日志是排查问题的第一手资料不能不用但一定要给它戴上“枷锁”。7.4 容器内访问宿主机服务为什么连不上有个场景我折腾了很久容器里的应用需要访问宿主机上的某个服务我直接在容器里写了localhost结果怎么都连不上。原因其实清楚容器有自己的网络命名空间localhost指向的是容器自己不是宿主机。要访问宿主机服务应该用宿主机的IP地址。如果你用的是桥接网络需要查一下宿主机在Docker网络里的地址如果用了host模式倒是可以直接用localhost但那种模式本质上已经把容器和宿主机的网络融在一起了使用场景有限。遇到这类连接问题先想清楚“网络命名空间”的边界在哪里排查速度会快很多。7.5 镜像体积膨胀和敏感信息泄露风险构建镜像时如果不够克制镜像体积很容易膨胀。安装包、缓存文件、临时文件都会被带进镜像每个镜像都堆满垃圾。更严重的是如果直接把密钥写进Dockerfile或者复制进去这些敏感信息会随着镜像层层保留后面推送到仓库就等于泄露。我总结出几条实用建议能用一条命令完成的安装尽量合并减少中间层安装完了及时清理缓存和临时文件避免在镜像里放私钥和配置文件。镜像不是越全越好而是越精简、越可重现越好。8. 下一步学习建议与个人心得清单8.1 先别急着上编排平台学完基础之后很容易听到各种编排平台和容器集群的概念然后着急去追热点。我建议还是先稳住把单机环境下的容器操作练熟理解镜像构建、数据持久化、网络连接再用编排工具把多个容器组织起来。编排平台确实很强大但它解决的是“容器多了之后怎么调度、怎么扩容、怎么保证高可用”的问题。如果你的场景还只是几个容器先掌握单机层面的基础反而更实用。我从基础跳到编排时因为概念没打牢吃足了苦头。先把地基压实后面再过渡会顺畅得多。8.2 常用命令速查卡目的命令示例拉取镜像docker pull 镜像名:版本查看本地镜像docker images后台运行容器docker run -d -p 宿主机端口:容器端口 --name 容器名 镜像名查看运行中容器docker ps查看全部容器docker ps -a查看日志docker logs -f --tail 100 容器名进入容器docker exec -it 容器名 sh停止容器docker stop 容器名删除容器docker rm 容器名删除镜像docker rmi 镜像名构建镜像docker build -t 镜像名:版本 .查看容器详情docker inspect 容器名这张表不需要刻意背。用得多了自然会记住。遇到记不清的参数再回来查就好。8.3 我的几条日常习惯用得时间长了我慢慢形成了一些固定的操作习惯。建镜像前先想好标签和版本号避免全部堆成latest有状态的服务启动前就决定数据卷怎么挂而不是等数据丢了再后悔每次调试容器都尽量通过修改配置和镜像来改变行为保持在容器里手工操作只做应急。这些习惯看起来很平常但真的能让人少走很多弯路。还有一个小技巧把平时常用的开发环境模板做成镜像保存起来新电脑上一条命令就能还原整套开发环境省去了重复安装配置的时间。这也是我越来越觉得Docker值得花时间学习的原因所在——它不只是部署工具更是一种把环境固化、随身携带的工作方式。