1. 内容整体设计与思路拆解很多朋友第一次接触 Docker 时,见到命令第一反应就是去背参数。我之前带过几个新人,拿着命令表格啃了一周,真到部署项目时还是手忙脚乱。原因很简单:Docker 本身的命令体系并不算复杂,难的是理解每条命令在“镜像、容器、数据、网络”这条主线上扮演什么角色。这篇文章我不会机械地按字母表一条条列命令,而是按日常使用的真实场景拆解。把命令当成一套工具:什么时候去构建镜像、什么时候只需拉起容器、什么时候必须把数据卷挂载出来、日志刷屏了又该从哪里排查。按这条思路走下来,你会发现绝大多数操作就那么几十条命令,而其中高频使用的可能只有十几条。先说一个核心概念:镜像和容器。把镜像理解成一个“安装包”,容器则是安装包运行起来后的“进程实例”。Dockerfile 负责把构建过程写清楚,docker build把代码和依赖打包成镜像,docker run把镜像实例化成容器,docker exec进入运行中的容器做检查,docker commit把某个容器当前状态固化成新镜像。这套流程就是 Docker 使用的完整闭环,理解它比背诵一百个参数更有效。Docker 命令还有一个特点:它分为客户端命令和守护进程(daemon)命令,我们日常敲的所有 docker 开头的命令都是客户端命令,通过 REST API 与后台守护进程通信。这就解释了为什么某些命令(比如 docker build 时挂载 SSH agent)在不同操作系统上表现不一致——本质上是客户端与 daemon 之间的权限和 socket 通信差异。懂得这一点之后,遇到“命令明明存在但响应诡异”的情况,你会先检查 daemon 日志,而不是反复重敲同样的命令。再聊一个很多人忽视的角度:Docker 命令的“可组合性”。命令不是孤立的,docker ps查到容器 ID,然后docker logs去查日志,接着docker exec进去看现场,最后docker inspect拉取完整元数据。把这条链路练熟,排障效率会有质的提升。下面各章节我会按这条实战路径展开,尽量保证每条命令都能落到“你什么时候会用它、为什么用、不用会怎样”三个问题上。2. 核心命令解析与实操要点2.1 镜像管理:构建、拉取、查看与清理镜像管理是整个 Docker 操作的基石。先记住一条原则:不要凭感觉拉镜像,先明确版本,再考虑大小和安全。最基础的工作从拉取镜像开始:docker pull nginx:stable-alpine docker pull postgres:16.2这里尤其要提醒“默认 tag 陷阱”。不加 tag 时 Docker 默认拉取 latest,而 latest 是一个滚动标签,内容随时可能变化。生产环境如果拉了个 latest 然后直接上(生产服务器),过段时间再 deploy 同样的命令,拉下来的镜像可能已经不是同一个内容,这会带来非常隐蔽的复现问题。所以我建议项目里所有镜像引用都锁定具体 tag,或者至少在 CI/CD 里固定镜像摘要(digest)级别。接下来查看本地镜像:docker images docker image lsdocker images和docker image ls作用一致,我习惯用docker images,少敲几个字符。输出里的 TAG 列、IMAGE ID 列和 SIZE 列,在后续清理、对比镜像时都用得上。注意 IMAGE ID 是短 ID,同一镜像的不同 tag 可能共用同一个 ID,因为底层镜像层是一样的。如果想查看镜像细节,尤其是入口命令、暴露端口、环境变量这些元数据,用 inspect:docker inspect nginx:stable-alpine docker inspect --format '{{.Config.ExposedPorts}}' nginx:stable-alpine这里我插一句经验:用 --format 拉取单个字段比直接看全量 JSON 高效得多。排障时经常只需要确认某一个端口或某一个 env,全量输出很长很乱,格式化输出一下子就能定位。Go template 语法不算难,最常用的就是{{.Config.Image}}、{{.State.Status}}、{{.NetworkSettings.IPAddress}}这几个路径。镜像构建的命令行是:docker build -t myapp:1.0.0 . docker build --no-cache -t myapp:1.0.0 . docker build --target builder -t myapp:1.0.0 .-t是打 tag,--no-cache跳过构建缓存,--target在多阶段构建时指定中间阶段。多阶段构建是控制镜像体积的利器:第一阶段装编译工具链,第二阶段仅拷贝编译产物和运行依赖,最终镜像可以小到原来的十分之一。这一步的价值在日常部署中特别明显,镜像小,推送就快,拉取就快,启动也快。镜像清理是承载“大而全”思想的反面教材。经常出现的情况是:磁盘被一堆悬空镜像和无用缓存塞满,然后整个宿主机的磁盘警报。清理命令如下:docker image prune docker image prune -a docker system dfdocker image prune只删除没有被容器引用的悬空镜像,-a则把所有未被使用的镜像全部清理。执行前最好先跑docker system df看一下空间占用分布,因为 Docker 的磁盘占用不只是镜像本身,还有 build cache、容器可写层、数据卷。盲目清理很容易把想保留的缓存也删掉。有一个坑我踩过好几次:容器已经删除了,但镜像还是删不掉。原因往往是还有中间层被另一个镜像引用,或者有停止状态的容器还持有这个镜像。这时候可以先用docker ps -a把所有容器列出来,把不需要的容器删干净,再执行docker image prune -a,大部分问题都能解决。2.2 容器生命周期:从创建到销毁的完整闭环容器生命周期管理,是我个人认为最核心也最容易被忽略的部分。很多人会用docker run起容器,但不太清楚 run 和 create、start 之间的差别。简单讲,docker run是“创建+启动”的复合操作,docker create只创建不启动,docker start启动已存在的容器。这个差别在批量初始化或需要先配置再启动的场景下很有用。创建并启动一个常规容器:docker run -d --name web-server -p 8080:80 -e ENV=production nginx:stable-alpine docker run -it --rm ubuntu:22.04 bash重点解释几个参数:-d:后台运行,容器不挂在前台终端。--name:给容器起名字,后续用名字操作比用容器 ID 方便得多。-p:端口映射,宿主机端口:容器端口。这里有个容易踩坑的点:-p 8080:80会把宿主机的所有网络接口都绑定到 8080,如果是多网卡机器要注意安全,更严谨的写法是-p 127.0.0.1:8080:80只绑定回环地址。-e:注入环境变量,应用读取配置的常见方式。-it:交互式终端。-i保持标准输入打开,-t分配伪终端。只敲docker run ubuntu bash经常会看到 bash 立即退出,就是因为没有分配终端。--rm:容器退出后自动删除。适合临时跑命令,避免生成一堆垃圾容器。查看当前运行的容器:docker ps docker ps -a docker ps -a --filter "status=exited" --format "table {{.Names}}\t{{.Status}}"docker ps只看运行中的容器。排障时几乎一定会用到-a,因为很多出问题的容器已经退出或处于异常状态。--filter按状态筛选,配合--format自定义输出列,在容器数量多的时候特别有意义。停止与删除:docker stop web-server docker kill web-server docker rm web-server docker rm -f web-serverstop是优雅停止,发送 SIGTERM 让进程自己清理退出;kill是强行终止,直接发 SIGKILL。如果 stop 之后进程没退,可以考虑等几秒再 kill。rm删除容器,rm -f是强制删除,即使容器正在运行也会先 kill 再删。这里有个细节:停止一个容器并不会自动删除它,停止的容器依然占用磁盘空间(容器可写层还在)。所以如果你确认一个容器不会再用了,记得 rm 掉,别留在那儿占地方。进入运行中的容器:docker exec -it web-server bash docker exec web-server ls /etc/nginx docker attach web-serverdocker exec -it web-server bash是登录容器的标准姿势。docker attach是附着到容器主进程的输入输出上,容易把终端搞乱,一般不推荐日常使用。还有一个常见误区是试图用docker run再起一个容器来进旧容器,这是行不通的思路,要记住 exec 才是在已有容器里执行命令的正确工具。容器和宿主机之间拷贝文件:docker cp ./config.yml web-server:/app/config.yml docker cp web-server:/var/log/app.log ./app.logdocker cp 在调试阶段很有用,比如临时往容器里塞一个配置文件,或者从容器里把日志拉出来分析。但要注意,这个操作不是数据持久化的替代品,容器一旦删除,里面的文件就全没了,所以真正需要长期保存的数据还是要用数据卷。容器状态查看:docker stats docker top web-server docker inspect web-serverdocker stats实时展示容器资源占用,跟系统 top 类似;docker top查看容器内进程;docker inspect查看完整配置和状态信息。这几个配合起来,能快速定位是 CPU 爆了、内存不够,还是进程崩溃重启。2.3 日志查看与实时跟踪日志是排查问题最重要的信息来源之一。Docker 提供的日志命令简单直接,但想用好还是有几个关键点要掌握。基础查看:docker logs web-server docker logs --tail 200 web-server docker logs -f web-server docker logs --since 10m web-server--tail 200只看最后 200 行,-f实时跟踪,--since 10m只看最近 10 分钟的日志。这三个参数组合使用,基本覆盖日常排障场景。比如一个服务突然报错,通常先docker logs --tail 500 --since 30m拉最近半小时的日志,再-f跟几秒看看实时输出。这里我要特别提醒一个很容易被忽略的问题:容器进程如果直接把日志输出到 stdout/stderr,docker logs 才能正常获取;如果进程自己写文件日志,docker logs 什么都看不到。前者是 Docker 官方推荐的方式,因为可以让日志统一走宿主机日志驱动,方便集中采集;后者则需要你通过 docker exec 进入容器去读文件,或者把日志目录挂载成数据卷再在宿主机上查看。很多自研应用默认写文件日志,部署到容器里发现 docker logs 没输出,这是非常常见的问题,不是 Docker 坏了。另外,日志驱动本身也值得了解。默认的 json-file 驱动会把日志存在宿主机上,长时间高并发服务的日志文件可能膨胀得很快。如果不对日志做轮转,最终会把磁盘塞满。可以在运行容器时加上日志轮转参数:docker run -d --name app \ --log-opt max-size=10m \ --log-opt max-file=3 \ nginx:stable-alpinemax-size 限制单个日志文件大小,max-file 限制最多保留几个文件,超出后会自动切割。这属于“用了之后才觉得值”的配置,建议默认就加上。否则等到磁盘报警再处理,往往已经错过了最佳排障时间。2.4 网络模式与端口映射网络这块是 Docker 命令里比较绕的一部分,但实际使用频率很高。Docker 默认有三种网络模式,理解它们就够了:bridge、host、none。bridge 模式是默认模式,容器通过虚拟网桥与宿主机通信,并用 NAT 实现对外访问。日常使用最多的就是 bridge。host 模式是容器共享宿主机网络栈,没有独立的 IP,端口也不需要映射,性能最好,但隔离性差。none 模式是禁用网络,一般用于一些特殊调试。查看网络列表和细节:docker network ls docker network inspect bridge创建自定义网络并让容器加入:docker network create my-net docker run -d --network my-net --name app1 myapp:1.0.0 docker network connect my-net app2自定义网络最核心的价值是容器间 DNS 解析。在同一个自定义网络里,容器可以直接用容器名访问对方,不需要记住 IP。比如 app1 要访问数据库容器 db,直接在配置里写http://db:5432就行,Docker 内置 DNS 会把 db 解析成对应容器的 IP。这比写死 IP 强太多了,因为容器重建后 IP 会变,用容器名可以避免到处改配置。端口映射这块有一个特别容易踩的坑:端口冲突。如果你在宿主机上已经有一个进程占用了 8080,再用docker run -p 8080:80就会报 bind: address already in use。排查方法很简单,先在宿主机上确认端口占用情况。另外还有一个小细节,-p 8080:80和-P(大写)是不同的,大写-P会把 Dockerfile 里 EXPOSE 的端口随机映射到宿主机高位端口,一般开发调试时图省事会用,生产环境不建议,因为端口不可控。端口映射还可以用--expose来标记但不映射。这个参数本身不产生实际端口映射,更像是一种元数据,告诉别人容器监听了哪些端口。说实话这个命令的实际作用有限,我一般只在写 Dockerfile 时用 EXPOSE,运行时不额外用它。2.5 数据卷与持久化容器本身是无状态的,容器删除后数据就没了。所以涉及数据库、缓存、上传文件等场景,必须使用数据卷(volume)把数据放到宿主机或外部存储上。数据卷的操作:docker volume create mydata docker volume ls docker volume inspect mydata docker volume rm mydata挂载数据卷运行容器:docker run -d --name db \ -v mydata:/var/lib/postgresql/data \ postgres:16.2这里-v mydata:/var/lib/postgresql/data把名为 mydata 的数据卷挂载到容器内的数据库数据目录。数据卷由 Docker 管理,存放在宿主机的特定目录下,使用起来最省心。还有一种方式是 bind mount,直接把宿主机的目录挂到容器里:docker run -d --name web \ -v /home/user/www:/usr/share/nginx/html:ro \ nginx:stable-alpinebind mount 适合开发环境想直接改代码立即生效的场景,或者要把运行日志落在宿主机路径的场景。:ro表示只读挂载,防止容器内写坏宿主机的目录。这里也要提一个容易混淆的点:具名卷和匿名卷的区别。具名卷就是我们上面写的mydata:/path,数据卷独立存在,容器删了数据卷还在。匿名卷只写路径不写名字,比如-v /var/lib/postgresql/data,Docker 会自动生成一个随机名字的卷。匿名卷在docker run --rm或删除容器时如果没注意,容易产生大量无主卷占用空间。所以生产环境我建议一律使用具名卷。备份和迁移数据卷,我常用的方法是临时起一个容器打包:docker run --rm -v mydata:/data -v /backup:/backup alpine tar czf /backup/mydata.tar.gz -C /data .这条命令把 mydata 卷的内容打包到宿主机的 /backup 目录。恢复时反向操作解压即可。这个思路本质上是用一个临时容器同时挂载数据卷和备份目录,执行 tar 命令。明白这个逻辑后,各种备份方式都可以灵活组合。3. 实操过程与核心环节实现3.1 从 Dockerfile 到运行的完整流程演示把一个项目从代码变成运行中的容器,完整流程是这样的:第一步,准备项目文件。假设有一个简单的静态网站,目录里包含 index.html 和一个 nginx 配置模板。第二步,编写 Dockerfile:FROM nginx:stable-alpine COPY index.html /usr/share/nginx/html/index.html EXPOSE 80这个 Dockerfile 很精简,实际项目可能还要涉及安装依赖、编译、权限调整等,但整体思路一致:从基础镜像出发,把项目产物拷进去,声明监听端口。第三步,构建镜像:docker build -t my-static-site:1.0.0 .构建成功后会输出镜像 ID,通过docker images可以确认。第四步,运行容器:docker run -d --name static-site -p 8080:80 my-static-site:1.0.0访问 http://localhost:8080 就能看到网站内容。若要修改发布版本,重新构建新 tag 的镜像,然后docker stop static-site && docker rm static-site && docker run ...换用新镜像即可。3.2 高效排障的组合命令链排障是我日常使用 Docker 命令最密集的场景。一套常用的组合拳如下:docker ps -a | grep myapp docker logs --tail 100 -f myapp docker exec -it myapp sh docker inspect --format '{{.State.Status}} | {{.State.ExitCode}}' myapp先用docker ps -a确认容器当前状态。如果是 Exited,看退出码,很多退出码能直接对应问题类型(比如 exit code 137 一般是 OOM 或被强杀,exit code 1 通常是应用启动时报错)。再docker logs看日志输出。如果日志信息不够,docker exec进容器查看文件系统、网络、进程状态。最后docker inspect获取完整元数据,比如挂载是否正常、环境变量是否齐全、网络 IP 是否正确。这套链路在实际排障里能覆盖八成以上的问题。有一个很典型的例子:服务本身起不来,日志里什么信息都没有,docker inspect 一看发现环境变量缺少必要配置,补上环境变量后容器秒起。很多问题根本不用进容器,元数据信息先扫一遍往往就有答案。另外要提醒一个实操技巧:容器如果一直在重启(restarting 状态),exec 是进不去的,但 inspect 和 logs 不受影响。所以遇到 restarting 状态,先看 logs,再 inspect 检查 entrypoint 和 command 是否正确,别急着强行 exec。3.3 镜像瘦身与多阶段构建镜像体积直接影响传输和部署效率。优化镜像体积最有效的手段是多阶段构建。举一个 Go 应用的例子:FROM golang:1.22 AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o myapp . FROM alpine:3.19 RUN apk add --no-cache ca-certificates COPY --from=builder /app/myapp /usr/local/bin/myapp ENTRYPOINT ["myapp"]第一阶段用完整的 golang 镜像编译产物,第二阶段用精简的 alpine 镜像只拷贝编译好的二进制文件。这样最终的镜像体积可能只有几十兆,而如果直接把 golang 镜像作为运行镜像,体积会多出几个 G。其他常用的瘦身技巧还包括:选择 alpine 版本的基础镜像,通常比标准版小很多。清理安装软件时留下的缓存文件。合并 RUN 指令,减少镜像层数。尽可能用官方镜像,官方镜像通常已经做了基础优化。这里有一个常见谣言要澄清:镜像层数少不等于体积一定小,关键是最终文件系统里有多少内容。多层构建会带来构建缓存方面的好处,而体积大小主要取决于最终层里的实际文件。所以别为了追求“单层”去写一堆 && 拼接,正常按可读性分段即可,同时注意及时清理无用文件。3.4 资源限制与稳定性配置生产环境跑容器,资源限制是必须考虑的。不加限制的容器可能把宿主机 CPU 和内存打满,影响同机其他服务。docker run -d --name app \ --memory=512m \ --memory-reservation=256m \ --cpus=1.5 \ nginx:stable-alpine--memory是硬限制,超过这个值容器会被 OOM。--memory-reservation是软限制,平时按这个值调度,紧急时可以超过。--cpus限制容器可用的 CPU 核数,可以写小数。查看容器资源占用和状态:docker stats app docker inspect --format '{{.State.OOMKilled}}' app如果容器频繁退出且退出码是 137,检查 OOMKilled 是否为 true,这是判断内存超限最直接的方法。我个人习惯在启动容器时就把内存限制设好,宁可给多一点上限,也不能完全不设上限,否则一台机器上的多个容器会互相拖垮。3.5 容器自动重启策略容器挂了怎么办?如果是开发环境,手动重启就行;生产环境最好让容器自己恢复。docker run -d --name app --restart unless-stopped myapp:1.0.0--restart的取值有 no、on-failure、always、unless-stopped。我的建议是除非特别明确,否则用unless-stopped:它在容器异常退出时自动重启,但在你手动 stop 之后不会自动拉起。这样既保证服务可用性,又不会违背“手动停就是要停住”的预期。always有个让人头疼的问题:即使你手动 stop 了,重启 Docker 服务时它还会再次自动启动,容易造成意外恢复。重启策略在 docker run 之后也可以调整:docker update --restart unless-stopped appdocker update 可以动态调整部分配置,比如重启策略、CPU 限制、内存限制。这个命令在不停机的情况下调整参数很实用,但不支持所有参数(端口映射和挂载就不能改)。4. 常见问题与排查技巧实录4.1 权限错误:permission denied在 Linux 上使用 Docker 时,经常会遇到:Got permission denied while trying to connect to the Docker daemon socket原因是用户不在 docker 用户组里。两种解决方式:一是把当前用户加入 docker 组(需要重新登录或执行newgrp docker),二是每次命令前加 sudo。我推荐用用户组方案,日常操作不必反复 sudo。但要注意,加入 docker 组等于把宿主机 root 级别的能力授权给你了,因为 Docker 本身提供了许多特权操作通道,这个权限本身需要被严肃对待。4.2 端口冲突处理docker run -d -p 8080:80 nginx:stable-alpine报错提示 bind: address already in use,说明宿主机 8080 已经被占用。排查占用进程:sudo ss -tlnp | grep 8080确认是被其他容器占用还是宿主机进程占用。如果是被容器占用,查看容器名后决定要不要停掉;如果是宿主机进程,考虑换一个宿主机端口,比如-p 8081:80。这里还有一个小技巧:可以先在宿主机上看到 8080 被 docker-proxy 进程占用,说明是一个容器映射了这个端口。4.3 镜像拉取超时与加速配置镜像拉取慢或超时,基本是网络问题。常见的解决方式:配置镜像加速器:修改 Docker daemon 配置文件(通常是 /etc/docker/daemon.json),加入 registry-mirrors 配置。调整拉取超时时间。注意修改 daemon.json 后需要重启 Docker 服务,然后docker info确认 Registry Mirrors 已生效。选择加速地址时,建议选择可用的公共镜像源。这里的核心是“加速器只是镜像源的一层转发”,如果某个源失效,要及时切换到备用源。4.4 容器时间与宿主机不一致容器默认使用 UTC 时区,而宿主机往往是本地时区。日志时间对不上会让排障变得非常痛苦。推荐方式是在启动容器时挂载时区文件:docker run -d --name app \ -v /etc/localtime:/etc/localtime:ro \ -e TZ=Asia/Shanghai \ myapp:1.0.0目前大部分基础镜像都内置了 tzdata,配合 TZ 环境变量就能正确显示本地时间。挂载 /etc/localtime 加:ro只读挂载,防止容器内变更影响宿主机。4.5 容器内 DNS 解析异常容器里域名解析失败,宿主机却正常,这种情况通常出在 DNS 配置上。默认情况下 Docker 使用宿主机的 DNS 配置。如果你所在的网络环境 DNS 特殊,可以在运行容器时指定 DNS:docker run -d --dns 8.8.8.8 --dns 114.114.114.114 --name app myapp:1.0.0排查时可以先docker exec进容器,查看 /etc/resolv.conf,确认 DNS 配置是否正常。有些场景是公司内网 DNS 与公共 DNS 混用,配置不当会导致部分域名解析失败。4.6 容器剩余可写层的空间不足容器运行过程中往可写层写数据,如果写的数据量很大,可能把宿主机的磁盘空间耗尽。容器本身没有独立的磁盘空间概念,可写层直接落在宿主机文件系统上。检查磁盘空间:df -h docker system df如果宿主机磁盘确实紧张,优先清理悬空镜像和停止的容器,同时排查是否有容器日志过大。这也是为什么之前强调加日志轮转参数的原因,日志膨胀是最常见的磁盘杀手。4.7 容器启动后立即退出容器启动后秒退是最常见的问题之一。原因可能是:前台进程缺失:容器主进程退出,容器就会退出。很多基础镜像默认执行的命令是 shell,shell 如果没有交互输入就会立刻退出,所以需要指定一个前台运行的进程(如 nginx -g "daemon off;")。应用启动报错:环境变量缺失、配置错误、端口冲突等。资源限制:内存不够被 OOM kill。排查顺序建议:先docker logs看输出,再docker inspect看 ExitCode 和 OOMKilled,必要时按 3.2 节的组合命令链进一步查。只要容器至少启动过,logs 里通常都会留下痕迹。4.8 docker compose 与命令的配合最后提一下 docker compose。写 docker compose 文件本质上是把 docker run 命令的参数结构化,对多容器编排尤其重要。它解决的是“命令太多记不住,配置重复敲太烦”的问题。一个简单的 compose 示例:version: "3.8" services: web: image: nginx:stable-alpine ports: - "8080:80" volumes: - ./html:/usr/share/nginx/html:ro restart: unless-stopped对应命令操作:docker compose up -d docker compose ps docker compose logs -f docker compose downdocker compose down 不会删除有名字的数据卷,这是设计上的安全考虑。如果你想连数据卷一起清掉,加-v参数,但一定要确认数据不需要保留,否则相当于删库跑路。回到最初的思路:命令是工具,不是目的。Docker 命令体系的本质是“用一套一致的抽象来管理镜像、容器、网络、数据”。你可以从最常用的十几条命令开始,逐步掌握更高级的组合用法,最终形成自己的排障路径。培养的习惯是:先搞懂命令操作的对象是什么,再背参数。这样不仅记得牢,遇到文档里没直接给出的场景时,也能自己推演出合理的命令组合。
1. 内容整体设计与思路拆解很多朋友第一次接触 Docker 时,见到命令第一反应就是去背参数。我之前带过几个新人,拿着命令表格啃了一周,真到部署项目时还是手忙脚乱。原因很简单:Docker 本身的命令体系并不算复杂,难的是理解每条命令在“镜像、容器、数据、网络”这条主线上扮演什么角色。这篇文章我不会机械地按字母表一条条列命令,而是按日常使用的真实场景拆解。把命令当成一套工具:什么时候去构建镜像、什么时候只需拉起容器、什么时候必须把数据卷挂载出来、日志刷屏了又该从哪里排查。按这条思路走下来,你会发现绝大多数操作就那么几十条命令,而其中高频使用的可能只有十几条。先说一个核心概念:镜像和容器。把镜像理解成一个“安装包”,容器则是安装包运行起来后的“进程实例”。Dockerfile 负责把构建过程写清楚,docker build把代码和依赖打包成镜像,docker run把镜像实例化成容器,docker exec进入运行中的容器做检查,docker commit把某个容器当前状态固化成新镜像。这套流程就是 Docker 使用的完整闭环,理解它比背诵一百个参数更有效。Docker 命令还有一个特点:它分为客户端命令和守护进程(daemon)命令,我们日常敲的所有 docker 开头的命令都是客户端命令,通过 REST API 与后台守护进程通信。这就解释了为什么某些命令(比如 docker build 时挂载 SSH agent)在不同操作系统上表现不一致——本质上是客户端与 daemon 之间的权限和 socket 通信差异。懂得这一点之后,遇到“命令明明存在但响应诡异”的情况,你会先检查 daemon 日志,而不是反复重敲同样的命令。再聊一个很多人忽视的角度:Docker 命令的“可组合性”。命令不是孤立的,docker ps查到容器 ID,然后docker logs去查日志,接着docker exec进去看现场,最后docker inspect拉取完整元数据。把这条链路练熟,排障效率会有质的提升。下面各章节我会按这条实战路径展开,尽量保证每条命令都能落到“你什么时候会用它、为什么用、不用会怎样”三个问题上。2. 核心命令解析与实操要点2.1 镜像管理:构建、拉取、查看与清理镜像管理是整个 Docker 操作的基石。先记住一条原则:不要凭感觉拉镜像,先明确版本,再考虑大小和安全。最基础的工作从拉取镜像开始:docker pull nginx:stable-alpine docker pull postgres:16.2这里尤其要提醒“默认 tag 陷阱”。不加 tag 时 Docker 默认拉取 latest,而 latest 是一个滚动标签,内容随时可能变化。生产环境如果拉了个 latest 然后直接上(生产服务器),过段时间再 deploy 同样的命令,拉下来的镜像可能已经不是同一个内容,这会带来非常隐蔽的复现问题。所以我建议项目里所有镜像引用都锁定具体 tag,或者至少在 CI/CD 里固定镜像摘要(digest)级别。接下来查看本地镜像:docker images docker image lsdocker images和docker image ls作用一致,我习惯用docker images,少敲几个字符。输出里的 TAG 列、IMAGE ID 列和 SIZE 列,在后续清理、对比镜像时都用得上。注意 IMAGE ID 是短 ID,同一镜像的不同 tag 可能共用同一个 ID,因为底层镜像层是一样的。如果想查看镜像细节,尤其是入口命令、暴露端口、环境变量这些元数据,用 inspect:docker inspect nginx:stable-alpine docker inspect --format '{{.Config.ExposedPorts}}' nginx:stable-alpine这里我插一句经验:用 --format 拉取单个字段比直接看全量 JSON 高效得多。排障时经常只需要确认某一个端口或某一个 env,全量输出很长很乱,格式化输出一下子就能定位。Go template 语法不算难,最常用的就是{{.Config.Image}}、{{.State.Status}}、{{.NetworkSettings.IPAddress}}这几个路径。镜像构建的命令行是:docker build -t myapp:1.0.0 . docker build --no-cache -t myapp:1.0.0 . docker build --target builder -t myapp:1.0.0 .-t是打 tag,--no-cache跳过构建缓存,--target在多阶段构建时指定中间阶段。多阶段构建是控制镜像体积的利器:第一阶段装编译工具链,第二阶段仅拷贝编译产物和运行依赖,最终镜像可以小到原来的十分之一。这一步的价值在日常部署中特别明显,镜像小,推送就快,拉取就快,启动也快。镜像清理是承载“大而全”思想的反面教材。经常出现的情况是:磁盘被一堆悬空镜像和无用缓存塞满,然后整个宿主机的磁盘警报。清理命令如下:docker image prune docker image prune -a docker system dfdocker image prune只删除没有被容器引用的悬空镜像,-a则把所有未被使用的镜像全部清理。执行前最好先跑docker system df看一下空间占用分布,因为 Docker 的磁盘占用不只是镜像本身,还有 build cache、容器可写层、数据卷。盲目清理很容易把想保留的缓存也删掉。有一个坑我踩过好几次:容器已经删除了,但镜像还是删不掉。原因往往是还有中间层被另一个镜像引用,或者有停止状态的容器还持有这个镜像。这时候可以先用docker ps -a把所有容器列出来,把不需要的容器删干净,再执行docker image prune -a,大部分问题都能解决。2.2 容器生命周期:从创建到销毁的完整闭环容器生命周期管理,是我个人认为最核心也最容易被忽略的部分。很多人会用docker run起容器,但不太清楚 run 和 create、start 之间的差别。简单讲,docker run是“创建+启动”的复合操作,docker create只创建不启动,docker start启动已存在的容器。这个差别在批量初始化或需要先配置再启动的场景下很有用。创建并启动一个常规容器:docker run -d --name web-server -p 8080:80 -e ENV=production nginx:stable-alpine docker run -it --rm ubuntu:22.04 bash重点解释几个参数:-d:后台运行,容器不挂在前台终端。--name:给容器起名字,后续用名字操作比用容器 ID 方便得多。-p:端口映射,宿主机端口:容器端口。这里有个容易踩坑的点:-p 8080:80会把宿主机的所有网络接口都绑定到 8080,如果是多网卡机器要注意安全,更严谨的写法是-p 127.0.0.1:8080:80只绑定回环地址。-e:注入环境变量,应用读取配置的常见方式。-it:交互式终端。-i保持标准输入打开,-t分配伪终端。只敲docker run ubuntu bash经常会看到 bash 立即退出,就是因为没有分配终端。--rm:容器退出后自动删除。适合临时跑命令,避免生成一堆垃圾容器。查看当前运行的容器:docker ps docker ps -a docker ps -a --filter "status=exited" --format "table {{.Names}}\t{{.Status}}"docker ps只看运行中的容器。排障时几乎一定会用到-a,因为很多出问题的容器已经退出或处于异常状态。--filter按状态筛选,配合--format自定义输出列,在容器数量多的时候特别有意义。停止与删除:docker stop web-server docker kill web-server docker rm web-server docker rm -f web-serverstop是优雅停止,发送 SIGTERM 让进程自己清理退出;kill是强行终止,直接发 SIGKILL。如果 stop 之后进程没退,可以考虑等几秒再 kill。rm删除容器,rm -f是强制删除,即使容器正在运行也会先 kill 再删。这里有个细节:停止一个容器并不会自动删除它,停止的容器依然占用磁盘空间(容器可写层还在)。所以如果你确认一个容器不会再用了,记得 rm 掉,别留在那儿占地方。进入运行中的容器:docker exec -it web-server bash docker exec web-server ls /etc/nginx docker attach web-serverdocker exec -it web-server bash是登录容器的标准姿势。docker attach是附着到容器主进程的输入输出上,容易把终端搞乱,一般不推荐日常使用。还有一个常见误区是试图用docker run再起一个容器来进旧容器,这是行不通的思路,要记住 exec 才是在已有容器里执行命令的正确工具。容器和宿主机之间拷贝文件:docker cp ./config.yml web-server:/app/config.yml docker cp web-server:/var/log/app.log ./app.logdocker cp 在调试阶段很有用,比如临时往容器里塞一个配置文件,或者从容器里把日志拉出来分析。但要注意,这个操作不是数据持久化的替代品,容器一旦删除,里面的文件就全没了,所以真正需要长期保存的数据还是要用数据卷。容器状态查看:docker stats docker top web-server docker inspect web-serverdocker stats实时展示容器资源占用,跟系统 top 类似;docker top查看容器内进程;docker inspect查看完整配置和状态信息。这几个配合起来,能快速定位是 CPU 爆了、内存不够,还是进程崩溃重启。2.3 日志查看与实时跟踪日志是排查问题最重要的信息来源之一。Docker 提供的日志命令简单直接,但想用好还是有几个关键点要掌握。基础查看:docker logs web-server docker logs --tail 200 web-server docker logs -f web-server docker logs --since 10m web-server--tail 200只看最后 200 行,-f实时跟踪,--since 10m只看最近 10 分钟的日志。这三个参数组合使用,基本覆盖日常排障场景。比如一个服务突然报错,通常先docker logs --tail 500 --since 30m拉最近半小时的日志,再-f跟几秒看看实时输出。这里我要特别提醒一个很容易被忽略的问题:容器进程如果直接把日志输出到 stdout/stderr,docker logs 才能正常获取;如果进程自己写文件日志,docker logs 什么都看不到。前者是 Docker 官方推荐的方式,因为可以让日志统一走宿主机日志驱动,方便集中采集;后者则需要你通过 docker exec 进入容器去读文件,或者把日志目录挂载成数据卷再在宿主机上查看。很多自研应用默认写文件日志,部署到容器里发现 docker logs 没输出,这是非常常见的问题,不是 Docker 坏了。另外,日志驱动本身也值得了解。默认的 json-file 驱动会把日志存在宿主机上,长时间高并发服务的日志文件可能膨胀得很快。如果不对日志做轮转,最终会把磁盘塞满。可以在运行容器时加上日志轮转参数:docker run -d --name app \ --log-opt max-size=10m \ --log-opt max-file=3 \ nginx:stable-alpinemax-size 限制单个日志文件大小,max-file 限制最多保留几个文件,超出后会自动切割。这属于“用了之后才觉得值”的配置,建议默认就加上。否则等到磁盘报警再处理,往往已经错过了最佳排障时间。2.4 网络模式与端口映射网络这块是 Docker 命令里比较绕的一部分,但实际使用频率很高。Docker 默认有三种网络模式,理解它们就够了:bridge、host、none。bridge 模式是默认模式,容器通过虚拟网桥与宿主机通信,并用 NAT 实现对外访问。日常使用最多的就是 bridge。host 模式是容器共享宿主机网络栈,没有独立的 IP,端口也不需要映射,性能最好,但隔离性差。none 模式是禁用网络,一般用于一些特殊调试。查看网络列表和细节:docker network ls docker network inspect bridge创建自定义网络并让容器加入:docker network create my-net docker run -d --network my-net --name app1 myapp:1.0.0 docker network connect my-net app2自定义网络最核心的价值是容器间 DNS 解析。在同一个自定义网络里,容器可以直接用容器名访问对方,不需要记住 IP。比如 app1 要访问数据库容器 db,直接在配置里写http://db:5432就行,Docker 内置 DNS 会把 db 解析成对应容器的 IP。这比写死 IP 强太多了,因为容器重建后 IP 会变,用容器名可以避免到处改配置。端口映射这块有一个特别容易踩的坑:端口冲突。如果你在宿主机上已经有一个进程占用了 8080,再用docker run -p 8080:80就会报 bind: address already in use。排查方法很简单,先在宿主机上确认端口占用情况。另外还有一个小细节,-p 8080:80和-P(大写)是不同的,大写-P会把 Dockerfile 里 EXPOSE 的端口随机映射到宿主机高位端口,一般开发调试时图省事会用,生产环境不建议,因为端口不可控。端口映射还可以用--expose来标记但不映射。这个参数本身不产生实际端口映射,更像是一种元数据,告诉别人容器监听了哪些端口。说实话这个命令的实际作用有限,我一般只在写 Dockerfile 时用 EXPOSE,运行时不额外用它。2.5 数据卷与持久化容器本身是无状态的,容器删除后数据就没了。所以涉及数据库、缓存、上传文件等场景,必须使用数据卷(volume)把数据放到宿主机或外部存储上。数据卷的操作:docker volume create mydata docker volume ls docker volume inspect mydata docker volume rm mydata挂载数据卷运行容器:docker run -d --name db \ -v mydata:/var/lib/postgresql/data \ postgres:16.2这里-v mydata:/var/lib/postgresql/data把名为 mydata 的数据卷挂载到容器内的数据库数据目录。数据卷由 Docker 管理,存放在宿主机的特定目录下,使用起来最省心。还有一种方式是 bind mount,直接把宿主机的目录挂到容器里:docker run -d --name web \ -v /home/user/www:/usr/share/nginx/html:ro \ nginx:stable-alpinebind mount 适合开发环境想直接改代码立即生效的场景,或者要把运行日志落在宿主机路径的场景。:ro表示只读挂载,防止容器内写坏宿主机的目录。这里也要提一个容易混淆的点:具名卷和匿名卷的区别。具名卷就是我们上面写的mydata:/path,数据卷独立存在,容器删了数据卷还在。匿名卷只写路径不写名字,比如-v /var/lib/postgresql/data,Docker 会自动生成一个随机名字的卷。匿名卷在docker run --rm或删除容器时如果没注意,容易产生大量无主卷占用空间。所以生产环境我建议一律使用具名卷。备份和迁移数据卷,我常用的方法是临时起一个容器打包:docker run --rm -v mydata:/data -v /backup:/backup alpine tar czf /backup/mydata.tar.gz -C /data .这条命令把 mydata 卷的内容打包到宿主机的 /backup 目录。恢复时反向操作解压即可。这个思路本质上是用一个临时容器同时挂载数据卷和备份目录,执行 tar 命令。明白这个逻辑后,各种备份方式都可以灵活组合。3. 实操过程与核心环节实现3.1 从 Dockerfile 到运行的完整流程演示把一个项目从代码变成运行中的容器,完整流程是这样的:第一步,准备项目文件。假设有一个简单的静态网站,目录里包含 index.html 和一个 nginx 配置模板。第二步,编写 Dockerfile:FROM nginx:stable-alpine COPY index.html /usr/share/nginx/html/index.html EXPOSE 80这个 Dockerfile 很精简,实际项目可能还要涉及安装依赖、编译、权限调整等,但整体思路一致:从基础镜像出发,把项目产物拷进去,声明监听端口。第三步,构建镜像:docker build -t my-static-site:1.0.0 .构建成功后会输出镜像 ID,通过docker images可以确认。第四步,运行容器:docker run -d --name static-site -p 8080:80 my-static-site:1.0.0访问 http://localhost:8080 就能看到网站内容。若要修改发布版本,重新构建新 tag 的镜像,然后docker stop static-site && docker rm static-site && docker run ...换用新镜像即可。3.2 高效排障的组合命令链排障是我日常使用 Docker 命令最密集的场景。一套常用的组合拳如下:docker ps -a | grep myapp docker logs --tail 100 -f myapp docker exec -it myapp sh docker inspect --format '{{.State.Status}} | {{.State.ExitCode}}' myapp先用docker ps -a确认容器当前状态。如果是 Exited,看退出码,很多退出码能直接对应问题类型(比如 exit code 137 一般是 OOM 或被强杀,exit code 1 通常是应用启动时报错)。再docker logs看日志输出。如果日志信息不够,docker exec进容器查看文件系统、网络、进程状态。最后docker inspect获取完整元数据,比如挂载是否正常、环境变量是否齐全、网络 IP 是否正确。这套链路在实际排障里能覆盖八成以上的问题。有一个很典型的例子:服务本身起不来,日志里什么信息都没有,docker inspect 一看发现环境变量缺少必要配置,补上环境变量后容器秒起。很多问题根本不用进容器,元数据信息先扫一遍往往就有答案。另外要提醒一个实操技巧:容器如果一直在重启(restarting 状态),exec 是进不去的,但 inspect 和 logs 不受影响。所以遇到 restarting 状态,先看 logs,再 inspect 检查 entrypoint 和 command 是否正确,别急着强行 exec。3.3 镜像瘦身与多阶段构建镜像体积直接影响传输和部署效率。优化镜像体积最有效的手段是多阶段构建。举一个 Go 应用的例子:FROM golang:1.22 AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o myapp . FROM alpine:3.19 RUN apk add --no-cache ca-certificates COPY --from=builder /app/myapp /usr/local/bin/myapp ENTRYPOINT ["myapp"]第一阶段用完整的 golang 镜像编译产物,第二阶段用精简的 alpine 镜像只拷贝编译好的二进制文件。这样最终的镜像体积可能只有几十兆,而如果直接把 golang 镜像作为运行镜像,体积会多出几个 G。其他常用的瘦身技巧还包括:选择 alpine 版本的基础镜像,通常比标准版小很多。清理安装软件时留下的缓存文件。合并 RUN 指令,减少镜像层数。尽可能用官方镜像,官方镜像通常已经做了基础优化。这里有一个常见谣言要澄清:镜像层数少不等于体积一定小,关键是最终文件系统里有多少内容。多层构建会带来构建缓存方面的好处,而体积大小主要取决于最终层里的实际文件。所以别为了追求“单层”去写一堆 && 拼接,正常按可读性分段即可,同时注意及时清理无用文件。3.4 资源限制与稳定性配置生产环境跑容器,资源限制是必须考虑的。不加限制的容器可能把宿主机 CPU 和内存打满,影响同机其他服务。docker run -d --name app \ --memory=512m \ --memory-reservation=256m \ --cpus=1.5 \ nginx:stable-alpine--memory是硬限制,超过这个值容器会被 OOM。--memory-reservation是软限制,平时按这个值调度,紧急时可以超过。--cpus限制容器可用的 CPU 核数,可以写小数。查看容器资源占用和状态:docker stats app docker inspect --format '{{.State.OOMKilled}}' app如果容器频繁退出且退出码是 137,检查 OOMKilled 是否为 true,这是判断内存超限最直接的方法。我个人习惯在启动容器时就把内存限制设好,宁可给多一点上限,也不能完全不设上限,否则一台机器上的多个容器会互相拖垮。3.5 容器自动重启策略容器挂了怎么办?如果是开发环境,手动重启就行;生产环境最好让容器自己恢复。docker run -d --name app --restart unless-stopped myapp:1.0.0--restart的取值有 no、on-failure、always、unless-stopped。我的建议是除非特别明确,否则用unless-stopped:它在容器异常退出时自动重启,但在你手动 stop 之后不会自动拉起。这样既保证服务可用性,又不会违背“手动停就是要停住”的预期。always有个让人头疼的问题:即使你手动 stop 了,重启 Docker 服务时它还会再次自动启动,容易造成意外恢复。重启策略在 docker run 之后也可以调整:docker update --restart unless-stopped appdocker update 可以动态调整部分配置,比如重启策略、CPU 限制、内存限制。这个命令在不停机的情况下调整参数很实用,但不支持所有参数(端口映射和挂载就不能改)。4. 常见问题与排查技巧实录4.1 权限错误:permission denied在 Linux 上使用 Docker 时,经常会遇到:Got permission denied while trying to connect to the Docker daemon socket原因是用户不在 docker 用户组里。两种解决方式:一是把当前用户加入 docker 组(需要重新登录或执行newgrp docker),二是每次命令前加 sudo。我推荐用用户组方案,日常操作不必反复 sudo。但要注意,加入 docker 组等于把宿主机 root 级别的能力授权给你了,因为 Docker 本身提供了许多特权操作通道,这个权限本身需要被严肃对待。4.2 端口冲突处理docker run -d -p 8080:80 nginx:stable-alpine报错提示 bind: address already in use,说明宿主机 8080 已经被占用。排查占用进程:sudo ss -tlnp | grep 8080确认是被其他容器占用还是宿主机进程占用。如果是被容器占用,查看容器名后决定要不要停掉;如果是宿主机进程,考虑换一个宿主机端口,比如-p 8081:80。这里还有一个小技巧:可以先在宿主机上看到 8080 被 docker-proxy 进程占用,说明是一个容器映射了这个端口。4.3 镜像拉取超时与加速配置镜像拉取慢或超时,基本是网络问题。常见的解决方式:配置镜像加速器:修改 Docker daemon 配置文件(通常是 /etc/docker/daemon.json),加入 registry-mirrors 配置。调整拉取超时时间。注意修改 daemon.json 后需要重启 Docker 服务,然后docker info确认 Registry Mirrors 已生效。选择加速地址时,建议选择可用的公共镜像源。这里的核心是“加速器只是镜像源的一层转发”,如果某个源失效,要及时切换到备用源。4.4 容器时间与宿主机不一致容器默认使用 UTC 时区,而宿主机往往是本地时区。日志时间对不上会让排障变得非常痛苦。推荐方式是在启动容器时挂载时区文件:docker run -d --name app \ -v /etc/localtime:/etc/localtime:ro \ -e TZ=Asia/Shanghai \ myapp:1.0.0目前大部分基础镜像都内置了 tzdata,配合 TZ 环境变量就能正确显示本地时间。挂载 /etc/localtime 加:ro只读挂载,防止容器内变更影响宿主机。4.5 容器内 DNS 解析异常容器里域名解析失败,宿主机却正常,这种情况通常出在 DNS 配置上。默认情况下 Docker 使用宿主机的 DNS 配置。如果你所在的网络环境 DNS 特殊,可以在运行容器时指定 DNS:docker run -d --dns 8.8.8.8 --dns 114.114.114.114 --name app myapp:1.0.0排查时可以先docker exec进容器,查看 /etc/resolv.conf,确认 DNS 配置是否正常。有些场景是公司内网 DNS 与公共 DNS 混用,配置不当会导致部分域名解析失败。4.6 容器剩余可写层的空间不足容器运行过程中往可写层写数据,如果写的数据量很大,可能把宿主机的磁盘空间耗尽。容器本身没有独立的磁盘空间概念,可写层直接落在宿主机文件系统上。检查磁盘空间:df -h docker system df如果宿主机磁盘确实紧张,优先清理悬空镜像和停止的容器,同时排查是否有容器日志过大。这也是为什么之前强调加日志轮转参数的原因,日志膨胀是最常见的磁盘杀手。4.7 容器启动后立即退出容器启动后秒退是最常见的问题之一。原因可能是:前台进程缺失:容器主进程退出,容器就会退出。很多基础镜像默认执行的命令是 shell,shell 如果没有交互输入就会立刻退出,所以需要指定一个前台运行的进程(如 nginx -g "daemon off;")。应用启动报错:环境变量缺失、配置错误、端口冲突等。资源限制:内存不够被 OOM kill。排查顺序建议:先docker logs看输出,再docker inspect看 ExitCode 和 OOMKilled,必要时按 3.2 节的组合命令链进一步查。只要容器至少启动过,logs 里通常都会留下痕迹。4.8 docker compose 与命令的配合最后提一下 docker compose。写 docker compose 文件本质上是把 docker run 命令的参数结构化,对多容器编排尤其重要。它解决的是“命令太多记不住,配置重复敲太烦”的问题。一个简单的 compose 示例:version: "3.8" services: web: image: nginx:stable-alpine ports: - "8080:80" volumes: - ./html:/usr/share/nginx/html:ro restart: unless-stopped对应命令操作:docker compose up -d docker compose ps docker compose logs -f docker compose downdocker compose down 不会删除有名字的数据卷,这是设计上的安全考虑。如果你想连数据卷一起清掉,加-v参数,但一定要确认数据不需要保留,否则相当于删库跑路。回到最初的思路:命令是工具,不是目的。Docker 命令体系的本质是“用一套一致的抽象来管理镜像、容器、网络、数据”。你可以从最常用的十几条命令开始,逐步掌握更高级的组合用法,最终形成自己的排障路径。培养的习惯是:先搞懂命令操作的对象是什么,再背参数。这样不仅记得牢,遇到文档里没直接给出的场景时,也能自己推演出合理的命令组合。