ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Docker命令实战:镜像、容器、数据卷与网络全解析

Docker命令实战:镜像、容器、数据卷与网络全解析 我第一次接触 Docker 时的第一反应是这命令也太多了。装好 Docker Desktop界面上有个图形面板能点点点可真到了服务器上、到了 CI 流水线里能依赖的只有终端。后来慢慢用多了才明白Docker 命令虽然清单很长背后的主线其实非常收敛——镜像、容器、网络、数据卷再加一个构建。把这条主线理清楚日常开发里九成以上的场景都能靠终端搞定剩下那一成也都逃不出logs、inspect、prune这几个排查命令的兜底范围。这篇文章我不想把docker --help的输出抄一遍而是打算按真实操作链路重新组织把“你真正会反复敲的命令”和“出问题时才用上的命令”分清楚再结合我实际部署 MySQL 8.0、Redis 主从、GitLab 这些常见应用时踩过的坑尽可能把每条命令背后的“为什么”也说清楚。内容面向已经装好 Docker、想系统把命令用顺手的同学也适合正在排查疑难杂症的老人。1. 镜像操作基本功pull、images、tag 与清理的完整逻辑1.1 拉取镜像时要知道的命名规则docker pull是大多数人接触 Docker 的第一条命令但很多人只是照着教程敲了个docker pull nginx不清楚这背后到底发生了什么。完整写法其实是这样的docker pull nginx:1.25-alpine拆开看就是三部分镜像仓库地址 镜像名 标签。没写仓库地址时默认去 Docker Hub 拉没写标签时默认取latest。这里有个新手最容易掉的坑——latest只是一个标签不代表“最新版”更不代表“稳定版”。很多镜像仓库维护者会固定把一个滚动版本标记为latest比如mysql:latest现在指向 8.0 系但将来可能变成 9.x你在生产环境依赖这个标签将来某天重新部署时拉到的镜像可能完全不一样。所以我的习惯是任何上生产的镜像一律写全标签。在公司内部通常还会带上私有仓库地址比如docker pull registry.internal.example.com/team/nginx:1.25-alpine这条命令敲下去Docker 会先查本地是否存在这个镜像没有就按仓库地址去拉。国内访问公共仓库经常遇到“镜像下载慢”的问题最常见的解法是在/etc/docker/daemon.json里配置镜像加速器改完记得systemctl restart dockerWindows/Mac 的 Docker Desktop 则在设置面板里直接配。如果公司有内部镜像仓库优先走内网速度和稳定性都会好很多。1.2 查看本地镜像images 与 inspect 的区别docker images和docker image ls是一条命令的两种写法后者是 Docker 官方推荐的子命令风格。输出结果里有仓库名、标签、镜像 ID、创建时间和大小足够应付日常。更关键的是docker inspect。它能把一个镜像的完整元数据以 JSON 形式打出来包括暴露的端口、默认命令、作者、环境变量、架构等。我最常用的一个场景是排查“为什么容器起不来”docker inspect --format {{.Config.Env}} mysql:8.0这样只输出环境变量部分比盯着一整坨 JSON 舒服得多。还有个容易被忽略的命令是docker historydocker history --no-trunc mysql:8.0它能展示镜像的每一层是怎么构建出来的。当你发现一个镜像莫名其妙很大用docker history往往能找到是哪一个RUN命令把大量没清理的依赖写进了镜像层里。1.3 rmi 与 prune清理镜像时千万别手滑删镜像是docker rmi 镜像ID或名称但很多人删不掉报错提示“image is being used by container”。这个报错的意思是有一个容器哪怕是已停止的正引用着这个镜像。解决办法要么先把容器删掉要么用-f强制删除。-f能解决报错但我不建议把它当常规手段因为强制删除后留下的“悬空镜像”还是得回头清理。批量清理才是重点。docker image prune能清理所有悬空镜像即没有标签、也没有被容器引用的镜像加-a会连那些“没有被任何运行中容器使用”的镜像一起清掉。我对-a的态度是个人开发机无所谓生产服务器上慎用。因为“没有运行中容器使用”不代表“以后不会再被使用”一次手滑就可能把刚打好、还没来得及部署的镜像删光。2. 容器生命周期管理创建、进入、退出与日志定位2.1 一条 run 命令背后的常用参数组合docker run是所有命令里参数最密集的一个把它的参数吃透Docker 基本就学会了一半。常见组合如下参数作用使用场景-d后台运行不阻塞终端启动 MySQL、Nginx 等服务-it交互式 伪终端进入容器执行操作等价于你坐在容器里的终端前--rm容器退出后自动删除临时测试、一次性任务避免遗留容器--name给容器命名方便后续用名字管理而不是记容器 ID-p宿主机端口:容器端口把容器端口暴露到宿主机-v或--mount挂载数据卷或宿主机目录数据持久化-e设置环境变量给容器里的应用传配置--network指定网络让容器加入自定义网络--restart重启策略no、on-failure、always、unless-stopped举一个最典型的例子docker run -d --name nginx-web -p 80:80 --restartunless-stopped nginx:1.25-alpine这里-d让它后台运行--name叫它nginx-web-p 80:80把容器的 80 端口映射到宿主机 80--restartunless-stopped保证机器重启后容器能自动恢复。这条命令足以覆盖 Web 服务的日常启动需求。新手最容易困惑的是docker run和docker create的关系。create只是创建容器但不启动run等价于create start。实际运维中create用得很少但写脚本时偶尔需要先建好容器确认网络、数据卷等配置无误后再启动这时候就有用了。2.2 查看容器状态ps、top 与 statsdocker ps查看正在运行的容器docker ps -a查看包括已停止的所有容器。我个人还习惯加-n 5只显示最近创建的 5 个避免机器上容器一多就看花眼。想知道容器内部在干什么两条命令很有用docker top 容器名 docker stats --no-streamdocker top能看到容器内实际跑着的进程作用和宿主机上的top类似。docker stats则能看每个容器的 CPU、内存、网络 I/O 和磁盘 I/O不写--no-stream时它会像top一样持续刷新想只看一眼当前快照就加上--no-stream。我排查“哪个容器把机器内存吃满了”时第一反应就是docker stats --no-stream。2.3 exec 与 attach进容器的多种姿势容器跑起来之后想进去看文件、跑命令行docker exec是最常用的方式docker exec -it nginx-web bash-it在前面讲 run 时出现过这里同样重要-t分配伪终端-i保持标准输入打开。只敲docker exec nginx-web bash大概率看到的是个没有交互效果的输出因为 bash 没有拿到 tty。docker attach是另一条进容器的命令和exec有本质区别attach是挂到容器主进程PID 1的标准输入输出上你看到的是主进程的日志和输出exec是重新开一个进程。所以调试时绝大多数情况用execattach只在你需要观察主进程的实时输出时才用得上。这里有个必须记住的细节在 attach 之后的终端里按 CtrlC信号会发给主进程很可能直接把容器停掉。想分离又不终止容器用快捷键 CtrlP 然后 CtrlQ。2.4 用 logs 定位问题按时间、按行数与实时跟踪容器起不来或者启动后立刻退出第一件事永远是看日志docker logs 容器名 docker logs -f --tail 300 容器名 docker logs --since 15m 容器名-f表示持续跟随输出就像tail -f--tail 300表示只看最后 300 行日志量大时必备--since 15m只看最近 15 分钟的日志排查“刚才发生过什么”时很高效。需要说明的是docker logs只对默认的json-file日志驱动有效。如果容器运行时分了--log-driversyslog或journald日志就不走 Docker 的 stdout 管道了得去对应的宿主机日志系统里查。这也是个让很多人排查半天找不到日志的隐藏坑。2.5 停止、重启与删除生命周期收尾停止容器用docker stop它会先给主进程发送 SIGTERM等 10 秒默认超时时间后再发 SIGKILL。所以有些容器“停不下来”不是卡住而是在等应用处理退出信号。等不及可以docker kill直接发 SIGKILL。重启用docker restart多数场景下等价于 stop start。但有个细节如果容器启动阶段要加载大量数据比如 GitLabrestart并不会刻意等待加载完成业务起来后是否真的就绪还是得看日志。删除容器用docker rm。发现容器正在运行用docker rm -f强删。批量清理所有已停止容器docker container prune -f这句话我再提醒一次它会删掉所有处于停止状态的容器。如果你有一个容器配置很复杂、只是暂时停掉准备明天再启动千万别对这台机器执行这条命令。3. 数据持久化与端口映射让容器数据不再一删就没3.1 绑定挂载、命名卷与 tmpfs三种持久化方案怎么选容器是临时性的默认情况下容器一删除内部产生的数据就没了。要持久化就得搞清楚三种方式持久化方式命令表达典型场景匿名卷由 Docker 自动生成卷名一般不建议用因为不好管理命名卷docker volume create mysql-data运行时-v mysql-data:/var/lib/mysql数据库数据、应用运行数据绑定挂载-v $(pwd)/config:/app/config配置文件、开发时代码实时更新tmpfs--tmpfs /tmp临时数据、要求不落盘的场景命名卷和绑定挂载是主力。开发时我特别推荐绑定挂载当前目录到容器里的工作目录这样宿主机改代码容器里立刻生效不用每次重建镜像docker run -d --name dev-app -v $(pwd):/app -w /app node:18 npm run dev-w /app把容器内工作目录切到挂载点进去时不用反复 cd。3.2 数据卷操作命令与目录所有权经验命名卷有一套独立的管理命令docker volume create mysql-data docker volume ls docker volume inspect mysql-data docker volume rm mysql-datadocker volume inspect会告诉你这个卷实际落在宿主机的哪个目录Mountpoint 字段。有时候你会直接摸到那个目录里去改文件但我要提醒不要随意改数据卷里的文件权限和所有者尤其是数据库目录改坏了可能导致 MySQL 启动失败。权限问题在绑定挂载场景下非常典型。MySQL 官方镜像里的 mysqld 进程以mysql用户运行UID 大概是 999如果你把宿主机上一个 root 所有的目录直接挂到/var/lib/mysql就可能报Permission denied。排这种问题我常用的套路是先把目录的所有者信息打出来docker run --rm -v $(pwd)/data:/data alpine ls -ln /data看看里面文件的 UID/GID再对应调整宿主目录权限。这是典型的“容器是进程视角宿主机是文件视角”的错位理解了也就好办了。3.3 端口映射-p、-P 与 host 模式的区别-p前面讲过宿主机端口:容器端口。这里有个容易被忽略的细节-p 3306:3306会把端口绑定到宿主机所有网卡接口包括公网接口。不想把数据库直接暴露到公网就写明绑定地址docker run -d --name mysql8 -p 127.0.0.1:3306:3306 mysql:8.0这样只有宿主机本地能访问外部机器连不上。很多安全事件都是因为少写了127.0.0.1前缀把数据库裸奔到了公网。-P大写是随机把容器暴露的端口映射到宿主机的随机高位端口配合docker port 容器名查看映射结果适合临时测试不建议上生产。还有一种思路是直接不用-p而是在启动时指定--network host。host 模式下容器直接共享宿主机网络栈不经过 NAT性能损耗小但端口冲突风险也由你自理。MySQL 这类连接密集的应用在一些压测场景确实有人用 host 模式但日常开发建议还是走 bridge 端口映射。3.4 端口查看与容器间通信想确认一个容器的端口映射情况除了docker ps还可以用docker port 容器名排查“宿主机的 3306 被谁占了”这类问题靠docker ps就能看到映射条目。但如果宿主机上还跑着非 Docker 的进程就得回到宿主机用ss -lntp或者lsof -i :3306去查这也是我经常用的“跨界排查”组合。4. 容器间的网络编排从默认 bridge 到自定义网络4.1 默认网络模式的适用边界Docker 安装后会默认创建三个网络bridge、host、none。默认情况下容器都会加入bridge网络容器与容器之间可以通过 IP 访问但有个很要命的限制同一个默认 bridge 网络里容器之间不能直接用容器名做 DNS 解析。很多人部署了应用和数据库在应用容器里配置数据库地址时写了mysql:3306结果连不上改成容器 IP 就能通这就是默认 bridge 导致的。解决方案不是去记 IP容器重建后 IP 会变而是创建自定义网络。4.2 自定义 bridge 网络create、ls、inspect、connect 与 rm自定义 bridge 网络的核心价值是自动 DNS 解析。先创建网络docker network create app-net启动容器时加入这个网络docker run -d --name mysql8 --network app-net mysql:8.0 docker run -d --name app --network app-net -p 8080:80 myapp:latest这样app容器里直接写mysql8:3306就能访问 MySQL不用关心它实际 IP 是什么。这是容器编排里的基础能力也是手工搭建多服务环境时替代 Docker Compose 网络管理的一种轻量方案。查看网络成员用docker network inspect app-net输出结果的Containers字段里列出了加入该网络的所有容器和对应的 IP。一个容器想中途加入某个网络不需要重建docker network connect app-net 另一个容器名反方向就是docker network disconnect。这个能力在排查网络问题时特别有用比如想临时把某个容器拉进测试网络看看能不能连通。4.3 通过 DNS 名称互联的实际验证方法判断自定义网络是否生效可以进入任意一个容器尝试解析另一个容器名docker exec -it app ping mysql8能 ping 通说明 DNS 解析和历史正常。再测试业务端口连通性docker exec -it app bash -c curl mysql8:3306对于 MySQL 端口curl 不一定会给出友好提示但只要有Connection refused之外的响应基本说明网络通路没问题接下来才该去查应用配置和数据库权限。4.4 跨主机网络与后续扩展跨多台宿主机的容器网络是另一套复杂体系默认情况下一台宿主机的 bridge 网络无法直接跨越到另一台宿主机生产环境通常会引入网络插件但对绝大多数开发、测试、单机生产场景自定义 bridge 端口映射 挂载卷已经够用了。等哪天真的需要多机编排再引入更重型的调度系统也不迟不要一开始就把复杂度堆上来。5. Dockerfile 与 build镜像构建的工程化实践5.1 build 命令的常用选项与构建缓存镜像除了从仓库拉大多数业务镜像还是得自己构建。最基础的用法docker build -t myapp:v1 .末尾的.是构建上下文路径不是 Dockerfile 的路径。Dockerfile 里写的COPY . /app只能扣到上下文路径里的文件所以把整个项目根目录作为上下文是常规操作但注意别把node_modules、target这类大目录塞进上下文否则每次构建都在传一堆无用文件慢到怀疑人生。可以在.dockerignore文件里排除。构建缓存是另一个提速关键。Docker 构建时每一行指令都会生成一层缓存只要某一行没变之后的层就能复用。所以把频繁变动的操作尽量往下放。比如先COPY package.json再RUN npm install最后COPY . .。如果顺序反了只要项目代码一变npm install就得重新跑一遍白白浪费几分钟。某些时候缓存反而是负担比如依赖源更新了但 Dockerfile 没变构建出来还是老依赖。这时候加--no-cache强制重新构建docker build --no-cache -t myapp:v1 .多阶段构建也是工程化里的重点。典型模式是先在一个带编译工具的基础镜像里完成编译再在干净的精简镜像里只拷贝编译产物最终镜像能小非常多。构建时用--target可以单独构建某个中间阶段调试时很方便docker build --target builder -t myapp:builder .5.2 save、load 与 commit镜像迁移与应急手段要把镜像从一台机器搬到另一台没有公共仓库通道的机器save和load是主力docker save myapp:v1 | gzip myapp.tar.gz docker load -i myapp.tar.gzgzip 压缩后传输体积能小不少。加载回来镜像名和标签都会保留直接docker run即可。docker commit可以把一个正在运行的容器冻结成新镜像。我知道很多人把它当“快速保存现场”的手段但我的意见很明确能不用就别用。commit出来的镜像不可重复构建别人无法从源码和 Dockerfile 重建出相同镜像将来你的自己也会忘记这个镜像里装了什么、改过什么。它只在极少数应急场景下比如要立刻保存一个正在调试的容器现场才有价值正常发布流程里要坚决避免。5.3 Compose 命令在工作流里的定位严格来说 Docker Compose 不是单纯的镜像/容器命令但它已经成了当前部署多容器应用的事实标准。门面命令不外乎这组docker compose up -d docker compose ps docker compose logs -f docker compose exec app bash docker compose downdown默认会删除网络数据卷如果没加-v参数会保留。实际部署中我建议把 MySQL、Redis、应用服务都写进docker-compose.yml这样比手工敲十几条docker run靠谱得多——配置即代码重启机器后一条up -d就能恢复整套环境。手工命令更多用在临时调试、查看单容器状态、清理资源这些场景。6. 高频故障处理日志、磁盘与权限三板斧6.1 日志排除法容器起来就退出的经典排查路径容器一启动就退出报错Exited (1)这是新手最常遇到的情况也是我处理故障时最依赖的组合拳docker ps -a docker logs 容器名docker ps -a确认容器的退出码退出码 0 表示正常退出非 0 基本就是应用报错。docker logs是第一步但有时候日志很干净什么也没打那就需要看更细的容器配置docker inspect 容器名重点看State.ExitCode、State.Error、Config.Entrypoint、Config.Cmd。很多应用容器退出是因为入口命令依赖某个环境变量没传或者挂载目录找不着文件。inspect能把这些问题一次性暴露出来。配合前面提过的-f模板语法可以只抽关键字段docker inspect --format {{.State.ExitCode}} | {{.State.Error}} 容器名6.2 system df 与 prune磁盘告警的应急与日常“Docker 占满了磁盘”恐怕是每台机器之后都会遇到的问题。排查第一件事docker system df它可以输出镜像、容器、本地卷、构建缓存分别占了多少空间一目了然。接着按需清理docker system prune docker system prune -a docker system prune -a --volumes这里我再强调一次-a会清理掉所有未被正在运行的容器使用的镜像--volumes会把所有未被使用的命名卷也删掉。命名卷里的数据库数据也会一并消失。典型悲剧是MySQL 容器停了数据卷还在执行了全量 prune --volumes整个数据库没了。所以清理前先docker volume ls确认一遍或者干脆给重要的卷打上标签用docker volume prune --filter label!keep做排除式清理。只清理构建缓存可以用docker builder prune -f这步比较安全最多下次构建慢一点不会误删镜像和卷。6.3 权限、时区与容器内工具缺失的处理现在跑 Docker 一般不建议直接用 root但把当前用户加入docker组后有个副作用拥有 docker 组权限的用户几乎等同于拥有宿主机 root 权限因为可以挂载任意目录进容器。所以公司里的运维规范如果比较严通常会严格控制谁能登录这台机器、谁能执行 docker 命令。时区问题是另一个高频坑。官方镜像大多默认 UTC 时区如果你的应用依赖本地时间跑起来就会发现日志时间差 8 小时。简单的解法是启动时加环境变量比如docker run -d --name app -e TZAsia/Shanghai myapp:latest容器里没有vim、没有ps、没有五花八门的排查工具也很正常。“把容器当成一台小虚拟机缺啥装啥”是错误姿势。正确的做法是宿主机和容器之间用docker cp拷文件用docker exec执行业务命令需要交互诊断时临时docker exec跑一个静态编译的busybox或curl进去都可以但别把修改写进镜像镜像应该保持精简和可重建。7. 真实场景组合拳MySQL 8.0、Redis 主从与常见 Web 应用7.1 部署 MySQL 8.0 并让宿主机客户端连接拉镜像、建网络、启动容器docker network create app-net docker run -d \ --name mysql8 \ --network app-net \ -p 127.0.0.1:3306:3306 \ -e MYSQL_ROOT_PASSWORDYourStrongPassword \ -e MYSQL_DATABASEdemo \ -v mysql-data:/var/lib/mysql \ mysql:8.0严格来说docker pull mysql:8.0可以省略docker run发现本地没有会自动拉取但显式拉一次的好处是能提前确认镜像存储才可以。密码放在命令行里会在 shell 历史留下痕迹日常学习可以生产建议用--env-file方式注入环境变量docker run -d --name mysql8 --env-file ./mysql.env mysql:8.0mysql.env文件里写MYSQL_ROOT_PASSWORDxxxx即可。启动后验证docker exec -it mysql8 mysql -uroot -p docker logs mysql8logs里能看到 MySQL 是否完成了初始化若挂载的目录权限不对日志一般也会报“Permission denied”之类的提示。7.2 Redis 主从三个容器组一个自定义网络Redis 主从是练习容器网络和重启策略很好的案例。先确保这三个容器都在同一个自定义网络中否则从节点没法通过容器名找到主节点docker run -d --name redis-master \ --network app-net \ --restartunless-stopped \ redis:7 redis-server --appendonly yes docker run -d --name redis-replica-1 \ --network app-net \ --restartunless-stopped \ redis:7 redis-server --replicaof redis-master 6379 docker run -d --name redis-replica-2 \ --network app-net \ --restartunless-stopped \ redis:7 redis-server --replicaof redis-master 6379旧版 Redis 使用--slaveof新版官方推荐--replicaof注意镜像版本对应的参数。验证主从状态docker exec redis-master redis-cli info replication看connected_slaves是不是 2master_link_status是不是up。这边的容器名redis-master在同一个自定义网络里可以被直接从节点解析换成默认 bridge 网络就不行你可以在实验里对比一下能更直观理解第 4 章讲的自定义网络的价值。7.3 GitLab 这类重型容器的启动细节GitLab 镜像体积大、启动慢数据卷非常大。手工启动建议这样写docker run -d --name gitlab \ -p 8929:80 \ -p 8922:22 \ --restartunless-stopped \ -v gitlab-config:/etc/gitlab \ -v gitlab-logs:/var/log/gitlab \ -v gitlab-data:/var/opt/gitlab \ gitlab/gitlab-ce:latest注意端口映射宿主机 8929 映射到容器 80宿主机 8922 映射到容器 22。首次启动可能要等 2-5 分钟不要用docker restart反复打断它耐心看docker logs -f gitlab。初始密码通常要跑到容器里看docker exec gitlab cat /etc/gitlab/initial_root_password如果你打算长期维护 GitLab我更推荐把它写进docker-compose.yml。原因很简单GitLab 环境变量多、卷多、端口多手工docker run敲错一个参数很难发现配置固化下来之后重建整套环境就是一条docker compose up -d的事。我的个人体会是Docker 命令再全真正需要肌肉记忆的也就那么几条run的常用参数组合、ps、logs、exec、inspect、prune。其余命令都是遇到问题时按图索骥查出来的。我到现在偶尔还会犯“本想重启 redis结果误删了别的容器”这种低级错误所以操作前多看一眼容器名删除前先docker ps确认一遍这个习惯比背任何命令大全都管用。
返回列表