Docker核心操作实战:镜像、容器、数据卷与网络配置详解 直接说Docker 这名字第一次见的时候我就觉得它跟“集装箱”这个概念脱不开关系。实际用下来也确实如此它就是给应用打造的标准化集装箱。这篇是系列的第二篇不会再去重复“什么是镜像什么是容器”这种基础定义了咱们直接上手干点实事把镜像、容器、数据、网络这四块最常用的操作捋清楚。看完这篇你有能力自己把一个小型应用跑起来并且理解背后每一步在做什么。读之前先纠正一个事标题里的“dokcer”是拼错了正确写法是 Docker不过不重要很多人第一次接触都会手滑记住 Decoder 是解码器Docker 是集装箱这俩别混就行。测试环境说明一句我用的是 Ubuntu 22.04 服务器Docker 版本 24.x。命令在大多数 Linux 发行版上通用如果你用的是 Mac 或者 Windows 的 Docker Desktop除了个别路径写法有点差异大逻辑完全一致。1. 内容整体设计与思路拆解这一篇的设计思路说白了就是四个字够用就行。Docker 的功能浩如烟海真要全铺开讲一本五百页的书都打不住。但普通开发者和运维真正高频使用的翻来覆去就是镜像、容器、数据卷、网络这四板斧。把这四样东西玩明白了日常工作中百分之八十的需求都能覆盖。我见过不少新手上来就啃官方文档啃了三天还停留在 pull 一个 hello-world 的地步为什么因为官方文档是参考手册不是教程。它把每个命令的每个参数都列得清清楚楚但不告诉你什么时候该用哪个。就像给你一本字典你背完了所有单词但还是写不出一句像样的话。这篇不同我按使用场景来组织内容让你知道“我要做什么”对应“我该用什么命令”正着学反着查都顺手。另一个重要的设计考量是避免过度抽象。很多人讲 Docker 喜欢一上来就说“Linux 内核的 namespace 和 cgroups 是底层支撑”这话没错但对一个只想把应用跑起来的人来说不但没有帮助反而增加了认知负担。这一篇的策略是先让你会用用熟了以后如果对底层有兴趣再去翻内核相关的资料。先解决“怎么用”再解决“为什么这么用”最后才是“内部到底怎么实现的”。三个层次对应三类读者这篇集中在第一层但会稍微碰一点第二层。在解读这个教程结构之前还得强调一个核心视角Docker 不是一个虚拟机它是一个进程管理器。这句话值得从一开始就刻在脑子里。你 docker run 一个 nginx不是像 VMware 那样虚拟出一个完整的操作系统再跑一个 nginx而是直接在你当前的 Linux 内核上启动了一个 nginx 进程只是这个进程看到了一个独立的文件系统、独立的网络栈、独立的进程空间。这个概念上的差异决定了后面所有操作的理解方式。比如容器里 kill 掉 PID 1 进程容器就停了因为容器本身就是那个进程比如容器里装的库和依赖不需要完整的操作系统来支撑所以镜像可以做到几十兆甚至几兆。把这个思维转过来后面每个命令你都能找到合理的解释。2. 镜像管理拉取、查看、删除的日常操作2.1 镜像加速与拉取镜像这块最重要的就是 source 问题。默认情况下docker pull 从 Docker Hub 拉取。Docker Hub 官方源在国内网络环境下有时候速度感人甚至超时。解决办法是配置镜像加速器。加速器的原理很简单它是一个 Docker Hub 的只读缓存代理。你 pull 一个镜像的时候它先检查自己的缓存有就直接给你没有就去上游拉回来存一份再给你。对于反复拉取的热门镜像加速效果非常显著。配置方式在 /etc/docker/daemon.json 里加上 registry-mirrors 字段然后重启 Docker 服务。vim /etc/docker/daemon.json文件内容{ registry-mirrors: [https://dockermirror.example.com] }保存后执行systemctl daemon-reload systemctl restart docker配置好以后验证一下docker info看到 Registry Mirrors 一栏有地址就说明生效了。注意网上加速器地址五花八门很多是个人或小团队维护的有一定安全风险。建议优先选择大厂提供的地址并且不要用加速器拉取涉及敏感数据的私有镜像私有镜像应该走公司内部的镜像仓库。拉取镜像本身的命令非常简单docker pull nginx:latest这里要说一个非常关键的 tag 问题。生产环境禁止使用 latest 标签这句话值得加粗。latest 是个移动标签它指向的镜像随时可能更新今天拉的和明天拉的可能不是同一个版本。真出问题了你想回滚都找不到原来那个镜像。正确的做法是指定明确版本号docker pull nginx:1.25.3至于这个版本号怎么知道有哪些去 Docker Hub 页面看 tags 列表或者用命令查询docker image ls --format table {{.Repository}}:{{.Tag}}这个命令会列出本地所有镜像的仓库名和标签一目了然。2.2 镜像的查看、删除与原理说明拉下来的镜像用 docker images 查看标准输出大概是这样的REPOSITORY TAG IMAGE ID CREATED SIZE nginx 1.25.3 a875f0e7e3e4 2 weeks ago 187MB redis 7.2.3 d7c8f4b5a1e2 3 weeks ago 117MB ubuntu 22.04 6a2f6a4f6a7b 4 weeks ago 77.8MB那个 IMAGE ID 是镜像的唯一标识前 12 位就够了。删除镜像用docker rmi a875f0e7e3e4或者按“仓库:标签”来删docker rmi nginx:1.25.3如果镜像被容器占用了会报错说 conflict让你先删容器。这时可以加 -f 强制删但我不建议最好先搞清楚是哪个容器在用停掉删掉再删镜像否则留下一堆悬空镜像慢慢就变成磁盘垃圾。这里要延伸讲一下镜像分层的理念。Docker 镜像不是一个大文件而是一层一层堆叠的每一层对应 Dockerfile 里的一条指令。比如一个镜像可能由底层 OS 层、运行环境层、应用代码层、启动命令层构成。拉取的时候 Docker 会检查本地已有的层如果有就直接复用只下载缺失的层。这就是为什么你第一次拉 ubuntu 会花点时间但之后再拉基于 ubuntu 的应用镜像会非常快因为底层那几层已经在了。这个设计有什么实际影响第一多个镜像共享底层磁盘占用比你想象的要小得多。第二构建镜像的时候把不常变动的层写在前头频繁改动的代码写在最后面能极大利用构建缓存加快构建速度。第三删除镜像的时候如果底层被另一个镜像共享那只是减少了引用计数不会立刻释放磁盘空间。理解了分层这些现象都不再神秘。2.3 镜像标签与导出除了拉取和删除镜像操作里还有两个实用功能打标签和导出导入。打标签的场景是一个人开发的时候随手打了个 docker run 测试后来觉得这个镜像状态不错想留个备份就可以docker tag nginx:1.25.3 myregistry.com/nginx:v1标签的完整格式是“仓库地址/命名空间/镜像名:版本号”。打标签不会真的复制一份镜像只是在同一个镜像 ID 上多挂了一个名字。往后你想推送到私有仓库这个标签就是推送目标。导出导入的场景就比较贴近离线环境了。公司内网机器不能连外网需要在能上网的机器上拉好镜像用 docker save 打包成 tar 文件拷到内网再 docker load 导进去docker save -o nginx.tar nginx:1.25.3 docker load -i nginx.tar注意区分 docker save 和 docker export。save 保存的是整个镜像的分层结构可以用来再 push 到仓库export 导出的是容器的文件系统快照丢失了历史分层一般用来做排查取证或者快速复制一个容器的文件状态两者不能混用。3. 容器生命周期管理从启动到删除的关键命令3.1 创建与启动容器的完整参数镜像只是“集装箱的模板”真正跑起来的叫容器。启动容器最核心的命令是 docker run它其实是“创建 启动”两步合并。用法非常多我挑一条实际常用的命令做完整拆解docker run -d -p 8080:80 --name web-test --restartalways -v /data/web:/usr/share/nginx/html nginx:1.25.3一个个参数说-d后台运行容器不会霸占当前终端。不加这个参数容器会以前台模式运行CtrlC 退出时容器也跟着停了。-p 8080:80端口映射。冒号左边是宿主机端口右边是容器内端口。外部访问这台服务器的 8080 端口流量会被转发到容器内部的 80 端口上。--name web-test给容器起个名字。不指定的话 Docker 会随机分配一个名字管理起来非常痛苦。--restartalways容器意外退出时自动重启。生产环境建议都用上至少用 on-failure:5表示非正常退出时自动重启最多五次。-v /data/web:/usr/share/nginx/html数据卷挂载宿主机目录和容器目录共享内容。后面专门讲。敲完这条命令后Docker 会先检查本地有没有 nginx:1.25.3 镜像没有就自动 pull然后创建一个容器并启动它。几秒后你 curl 一下宿主机 8080 端口就能看到 Nginx 默认欢迎页。这里有个特别值得说明的细节为什么端口映射可以随便定端口不用关心容器内服务实际监听的是哪个 IP 和网卡因为容器内每个服务默认监听 0.0.0.0也就是容器里的所有网卡。Docker 做端口映射时是在宿主机上用 iptables 规则做了 DNAT 转发把宿主机端口的报文目标地址重写成容器 IP 和容器端口。所以你在容器里配 80 端口宿主映射到 8080外界看来就是这台机器 8080 提供服务完全不需要关心容器内部的网络细节。3.2 查看容器状态与进入容器操作启动起来的容器用 docker ps 查看运行中的容器加 -a 可以查看所有容器包括已停止的docker ps docker ps -a输出里 STATUS 那一列含义比较丰富。Up 表示在运行后面括号里的数字是容器运行时长。Exited (0) 表示正常退出退出码 0 代表程序结束非 0 代码就表示异常退出了比如脚本出错、找不到配置文件、端口被占用等。看退出码是个非常有效的排错方式后面说问题排查还会提到。进入正在运行的容器一般两种方式一种是交互式docker exec -it web-test bash-it 是两个独立选项的组合-i 保证标准输入保持打开-t 分配一个伪终端。拆开说-i 让你能和容器内进程交互-t 让你看到带颜色的控制台输出。连起来用就相当于“给我一个容器内的可交互终端”。退出时输入 exit 或用 CtrlD这个操作会结束 bash 会话但不会停止容器本身因为容器里的主进程是 nginxbash 只是你临时加进去的附加进程。另一种方式是非交互式执行命令比如查看容器内的环境变量、检查配置文件docker exec web-test env docker exec web-test cat /etc/nginx/nginx.conf这特别适合写脚本做自动化检查不需要人工交互直接拿到输出结果。提醒生产环境容器里通常没有 vi/vim很多精简镜像连 bash 都没有只剩 sh。进入容器排查问题前先确认镜像默认 shell 是什么。alpine 系列镜像用的是 ash语法和 bash 略有差异。3.3 停止、启动与删除容器的正确姿势停止容器用docker stop web-testdocker stop 发送的是 SIGTERM 信号给应用优雅退出的机会比如先处理完当前请求再关闭。这就像跟一个人说“你手头的事弄完就下班吧”而不是直接拔电源。如果应用卡死等不及优雅退出再上强杀手段docker kill web-testkill 发送 SIGKILL直接强制结束进程。一般来说先 stop等个十来秒没反应再 kill。已经退出的容器要重新启动用 docker start它会沿用创建时的所有配置不需要重新写一遍参数docker start web-test注意 docker run 和 docker start 的区别。run 是新建容器并启动start 是启动一个已经存在的容器。很多人用混了总认为 start 不行再 run 一个结果容器越积越多。删除容器docker rm web-test默认情况下运行中的容器删不掉必须加 -f 强删或者先 stop 再 rm。docker rm -f 实现的是“先 SIGKILL 再清理”有一定风险可能造成数据没来得及落盘不建议在数据库这类容器上使用。用 docker container prune 可以一次性清掉所有已停止的容器docker container prune这个命令会问你是否确认输入 y 回车。注意它只删已停止的保你在跑的所以相对安全。还有一点比较容易被忽略容器日志的查看。容器服务出问题第一时间看日志而不是进容器瞎翻docker logs web-testlogs 命令默认输出全部日志加 -f 可以像 tail -f 一样持续跟踪输出加 --tail 200 只看最近 200 行docker logs -f --tail 200 web-test这个效率比 exec 进容器翻日志文件高一个数量级一定要养成先看日志的习惯。4. 数据持久化与网络连通生产级配置要点4.1 数据卷挂载的两种方式与选择逻辑容器天生是“临时”的删掉之后里面的所有数据都没了。想让数据在容器删除后还能保留就要用数据卷。Docker 提供两种主流挂载方式一种是 bind mount 直接挂宿主机目录一种是 managed volume由 Docker 管理的数据卷。实际使用中最常见的是 bind mount命令格式 -v 宿主机目录:容器目录docker run -d -p 8080:80 --name web-test -v /data/web:/usr/share/nginx/html nginx:1.25.3这行命令的意思是宿主机的 /data/web 目录映射到容器内的 /usr/share/nginx/html 目录。你改宿主机里这个目录的文件容器里立刻能看到两边完全同步。这对开发调试来说极其方便。bind mount 的一个特点是宿主机目录的路径必须是绝对路径而且目录不存在的时候 Docker 会自动创建。但要注意权限问题如果目录权限不对容器内进程写不进去会报 Permission denied。一个稳妥的姿势是提前建好目录并设置权限mkdir -p /data/web chown -R 1000:1000 /data/web这里 1000:1000 是容器内常用 UID/GID很多基础镜像里的进程默认就是用这个身份跑的。把宿主目录的属主改成这个就能绕开权限坑。另一种方式 named volume不指定宿主机路径由 Docker 在 /var/lib/docker/volumes/ 下维护docker volume create web-data docker run -d -p 8080:80 -v web-data:/usr/share/nginx/html nginx:1.25.3命名卷的好处是路径由 Docker 统一管理备份迁移也方便适合数据不由宿主机直接管理的场景比如数据库数据目录。两种方式的核心区别我整理成一张表对比项bind mountnamed volume指定方式-v /宿主机绝对路径:/容器路径-v 卷名:/容器路径数据存放位置由你指定例 /data/web/var/lib/docker/volumes/ 下方便程度适合开发调试直接改文件适合生产环境统一管理备份方式直接备份目录即可需要 docker run 临时挂载备份或直接进宿主目录拷贝权限特点目录权限由你完全掌控Docker 自动初始化权限较规范实际项目选哪种我的习惯是开发环境、需要频繁编辑配置文件的场景用 bind mount数据库、消息队列这类生产数据容器用 named volume。前者灵活后者规整避免在一堆宿主路径里迷路。4.2 容器网络的三种模式与端口映射细节Docker 容器默认的网络模式是 bridge也就是桥接模式。可以理解成 Docker 在宿主机上架了一座虚拟交换机每个容器插了一根网线接到这个交换机上容器之间通过虚拟交换机互通向外访问则通过宿主机的 NAT 转发。查看网络模式docker inspect web-test | grep NetworkMode默认情况下容器内的网络是 Docker 分配的虚拟 IP比如 172.17.0.2这个 IP 每次重建容器都可能变。所以容器之间通信不能依赖 IP要依赖服务名或者固定 IP 方案。这就引出了自定义网络。创建一个自定义 bridge 网络然后把容器加进去docker network create app-net docker run -d --name web --network app-net nginx:1.25.3 docker run -d --name app --network app-net myapp:latest同一自定义网络里的容器可以直接用容器名互相访问。比如 app 容器里访问 web 容器直接 curl http://web 就行Docker 内置的 DNS 会自动把容器名解析成对应 IP。这个能力太重要了微服务互相调用不用写死 IP容器重建后别的服务照常访问。那外部怎么访问容器内的服务靠端口映射就是前面说的 -p 参数。这里展开讲一个容易被忽略的点docker run -d -p 8080:80 --name web-test nginx:1.25.3这条命令其实相当于先分配了一个随机宿主端口给容器 SSH 端口做映射再把 8080 绑定上去。如果想绑定特定 IP比如只允许内网访问可以写成docker run -d -p 192.168.1.100:8080:80 --name web-test nginx:1.25.3这样只有从 192.168.1.100 这个地址访问 8080 才会转发到容器其他网卡上的 8080 是关闭的。安全性提升一个档次特别是在云服务器有几张网卡的时候很实用。4.3 容器互联的进阶玩法端口映射适合对外提供服务。容器之间的通信优先用自定义网络加上容器名解析这个前面提过。但还有一种老式玩法 --link必须提一下因为老项目里还经常见到docker run -d --name web --link mysql-container mysql:8.0--link 的本质是往 web 容器的 /etc/hosts 里写入一条 mysql-container 的 IP 记录并在环境变量里注入 MySQL 的连接信息。但自定义网络出现以后官方不推荐再用 --link 了因为相比之下自定义网络更灵活还支持动态解析。看到旧文档里写 --link 的建议心里翻译成“用自定义网络替代”。最后再说一个 host 网络模式。用docker run -d --network host --name web-test nginx:1.25.3容器不隔离网络直接共享宿主机的网络栈和端口。好处是没有 NAT 转发网络性能损耗极少坏处是端口冲突直接报 bind 错误而且要小心容器里的服务直接暴露在你的宿主机所有网卡上风险更高。一般只在追求极限网络性能或调试时用。5. 常见问题与排查技巧实录5.1 镜像拉取失败的黄金三问镜像拉取不下来的情况太常见了新手遇到就慌。其实排查路径非常固定按顺序检查三件事就懂了。第一网络能不能通到镜像仓库。在服务器上执行ping registry-1.docker.io如果 ping 不通或者丢包严重大概率是网络的问题检查 DNS 配置和防火墙必要时切换加速器。第二加速器是否配置并且生效。前面说过用 docker info 看 Registry Mirrors 有没有输出。如果配置了但没有输出检查 /etc/docker/daemon.json 的 JSON 格式是否正确逗号位置少写一格都会导致整个文件解析失败。第三镜像名和 tag 是否存在。有些镜像虽然存在但 tag 名写错了会直接报 manifest unknown。常见情况是把 nginx:1.25.3 写成了 nginx:1.25Docker Hub 上很多镜像不会为每个小版本单独打 tag你拉的时候要先去 Hub 页面确认 tags 列表。这一条虽然笨但确实能拦住一半以上的拉取失败。真要检查全部过程可以用 docker pull 加 -v 参数查看详细输出或者直接在命令后面加个 --debug 级别的日志不过一般用不到这么深。5.2 容器启动失败的常规步骤容器一直重启或者启动后秒退是最让人头疼的问题之一。但排查方法也是固定的。先看退出码。docker ps -a 里 STATUS 列会显示 Exited (1)、Exited (137) 这类信息。退出码 0程序正常运行结束不是异常容器退了是预期的。退出码 1一般程序错误最常见很多场景是配置文件错误或依赖缺失。退出码 137被 SIGKILL 杀掉通常是内存超出限制检查 -m 参数设置或者系统内存。退出码 139段错误一般是程序崩溃检查有没有用错架构的镜像。然后看日志前面说过的 docker logs这一步能给出最直接的报错信息。如果日志一片空白可能程序还没来得及输出就崩了这时候需要自己进容器手动跑一下排查docker run --rm -it 镜像名 bash这个命令会新建一个容器并以交互方式进入不依赖原来的启动参数。在这里面手动执行命令看看程序有没有报缺失文件、权限不够这类问题。还有一个比较容易忽视的坑是入口脚本没执行权限。有些镜像的用户自定义脚本 chmod 没加 x启动时直接 Permission denied这种问题只看日志可能很隐蔽最好是用 exec 检查脚本权限docker exec web-test ls -l /entrypoint.sh5.3 时区、编码等“小毛病”的快速修正容器时区问题属于经典入门坑。默认情况下很多镜像的时区是 UTC比北京时间慢 8 个小时。解决办法很简单启动时挂载宿主机的时区文件docker run -d --name app -v /etc/localtime:/etc/localtime:ro myapp:latest这个 -v 把宿主机时区文件映射进容器让容器用和宿主机一样的时间。如果容器里的进程是 Java 应用还要额外加上 TZ 环境变量docker run -d --name app -e TZAsia/Shanghai -v /etc/localtime:/etc/localtime:ro myapp:latestJava 特别认 TZ 这个环境变量不设置的话即使 /etc/localtime 对了JVM 默认时区可能还是 UTC日志时间照样不对。编码问题相对少但确实遇到过。容器里中文文件名变成乱码一般是系统 locale 没设置。可以在 Dockerfile 或运行时加上docker run -d --name app -e LANGC.UTF-8 myapp:latest5.4 日志膨胀与磁盘占满的处理Docker 容器日志不停增长能把宿主机磁盘撑爆这是运维老手都会遇到的坑。默认情况下Docker 不会限制容器日志文件的大小一个异常打印的进程能在几天内写满几十 GB。合理的处理方式是在 daemon.json 里配置全局日志轮转{ log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 } }max-size 表示单个日志文件超过 10MB 就轮转max-file 表示最多保留三个日志文件。这样总日志体积控制在 30MB 以内对绝大多数场景够用。配置改完新创建的容器生效已经存在的容器不受影响。要立即清理现有容器日志可以这样做truncate -s 0 /var/lib/docker/containers/*/*-json.log这个命令会把所有容器的日志文件清空为 0 字节不影响容器运行。不过这是应急招治标不治本长期还是靠轮转配置。5.5 容器内 root 身份的安全隐患默认情况下容器内是 root但 Docker 的安全模型下 root 也有不少限制不过还是不建议长期以 root 跑生产服务。在 Dockerfile 或运行时切换成非 root 用户是更稳的做法这属于镜像构建范畴这里只给个运行时思路docker run -d --user 1000:1000 --name app myapp:latest--user 1000:1000 表示容器内进程以 UID 1000、GID 1000 的身份运行。这个数字对应宿主机上的某个用户好处是容器内生成的文件在宿主机上有明确的属主方便后续管理。对于不想改镜像暂用的场景这个参数很灵。但要注意切换用户后容器内原来需要 root 权限才能读的文件可能就读不了了比如证书文件、配置文件等。一般配合 4.1 里的权限设置一起用效果最好。6. 综合实操组合所有知识点的小项目讲了这么多零散的命令和概念最后带你把它们串成一个完整的小项目。项目目标是跑一个带数据持久化的 Web 应用外加一个数据库让流量能正常进来。先创建自定义网络让两个容器可以互相解析docker network create demo-net然后启动数据库容器docker run -d \ --name demo-db \ --network demo-net \ -v db-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORDyourpassword \ mysql:8.0这里用 -v db-data 命名卷保证数据不随容器删除而丢失。-e 设置环境变量是 MySQL 镜像初始化 root 密码的固定姿势。等它启动隔一会儿执行 docker ps 检查 STATUS 是否变为 healthy。接着启动应用容器docker run -d \ --name demo-app \ --network demo-net \ -p 8080:80 \ -v /data/app-conf:/etc/nginx/conf.d:ro \ nginx:1.25.3应用容器里可以通过 http://demo-db 访问到数据库容器这就是自定义网络带来的好处。改 /data/app-conf 下边的配置文件重启容器或者 reload 一下应用就会加载新配置文件操作和容器解耦。访问测试curl http://localhost:8080看到 Nginx 默认页整个链路已经通了。这时候你可以故意做几个小动作验证前面的知识点docker stop demo-app docker restart demo-db docker rm -f demo-app删掉 app 后用同样的参数重新 run 一个因为网络和数据卷都在服务和数据不受影响。这就是 Docker 组合式部署最核心的体验中间件也好应用也好都是一次共建、随处再起的积木。真正上手操作过一遍以后你会发现 Docker 的常用操作没有多少翻来覆去就是这些命令但每个命令背后的数据流、网络流、生命周期管理理解了之后你就从“背命令”升格成了“懂机制”。下一篇我会接着讲镜像是怎么构建的到时候 Dockerfile 的每一行指令对应的镜像层是怎么生成的容器的资源限制怎么做那个部分又是另一片天地了。