有没有试过照着网上的教程敲了十来条 Docker 命令,容器是跑起来了,但过两天再看完全不知道哪条命令是干嘛用的?我见过不少同事让一个容器在后台跑了两三个月,某天磁盘阈值报警才发现光日志文件就攒了二十多 G。Docker 的命令体系表面上就是一堆动词加参数,真正难的是搞清楚每个命令背后操作的到底是什么对象、什么状态,以及出问题时该用哪条命令去定位。
这篇就把我日常开发中实际用 Docker 命令的经验梳理一遍,从镜像管理、容器生命周期、网络和数据卷,到故障排查和磁盘清理,全是最常碰到的场景,也会把文档里一般不写透的细节和踩过的坑一并讲清楚。读完不一定能让你成为 Docker 专家,但至少以后用命令的时候能知道自己在做什么,排查问题时也有条理。
1. 先搞懂命令操作的四类对象:镜像、容器、网络、数据卷
1.1 为什么很多新手会把命令记混
我刚开始用 Docker 的时候,最大的困惑就是docker run、docker start和docker restart这三个命令到底有什么区别。后来才想明白,不是命令本身难,而是没有在脑子里建立 Docker 的分层模型。Docker 的一切操作都围绕四类对象展开:镜像(image)、容器(container)、网络(network)、数据卷(volume)。
镜像是一个只读模板,类似装系统用的 ISO 文件,容器是基于镜像创建的运行实例,类似装好系统后正在运行的电脑。一个镜像可以启动多个容器,每个容器之间有独立的文件系统、网络栈和进程空间。网络负责容器与容器、容器与宿主机的通信,数据卷则解决容器删除后数据丢失的问题。
建立这个模型之后,再看命令就清晰了:docker image开头的操作镜像,docker container开头的操作容器,docker network和docker volume同理。早期的 Docker 命令风格比较随意,比如docker images、docker ps、docker rm都是没有对象前缀的简写,新手经常分不清docker rm删的是容器而docker rmi删的是镜像。新版本命令推荐使用docker container rm、docker image rm这种带对象前缀的风格,逻辑上更不容易出错。
1.2 新版命令风格和老版简写的取舍
以删除操作为例,老写法docker rm删除容器,docker rmi删除镜像,很多老手都在这上面翻过车。我曾经想把一个镜像删掉,结果先删错了容器,后来又漏删了镜像,导致磁盘空间没释放出来。现在 Docker 官方推荐的做法是使用对象前缀的形式:
docker container rm [容器ID或名称] docker image rm [镜像ID或名称] docker network rm [网络名称] docker volume rm [卷名称]老写法虽然敲起来省几个字母,日常用习惯了也没问题,但在写脚本或者做自动化的时候,带对象前缀的形式可读性更高,别人看脚本时一眼就能知道你操作的是什么。我个人的习惯是:交互式命令行里怎么顺手怎么来,但凡是落进脚本的内容,一律用完整写法。这样即使几个月后再回来看脚本,也不用猜某个缩写到底指的是什么。
还有一个容易混淆的点是docker ps和docker ps -a。docker ps只显示正在运行的容器,docker ps -a显示所有容器(包括已经停止的)。很多新手以为容器跑过一次就消失了,其实容器默认是保留的,停止之后仍然存在。如果忘记加-a,就看不到已经停止的容器,会误以为容器丢了。
2. 镜像相关的命令:拉取、构建、查看与清理
2.1 从仓库拉取镜像时的细节
拉取镜像是使用 Docker 的第一步,命令很简单:docker pull nginx。但有几个细节值得注意。
第一个是标签(tag)的选择。不写标签时默认拉取latest标签,这个标签指向最新版本,但问题在于它是个动态的浮标,今天拉和三个月后拉可能得到不同的版本。放在开发环境里无所谓,生产环境或需要稳定复现的环境必须指定具体版本,比如docker pull nginx:1.27.3。指定版本的好处有两个:一是镜像体积往往比 latest 小,二是可以避免某个版本突然更新导致行为变化。
第二个是镜像仓库地址的问题。默认拉取的是 Docker Hub 上的公共镜像,国内访问可能会遇到超时或者速度慢的情况。常见的解决方案是配置镜像加速器,在/etc/docker/daemon.json里加 registry-mirrors 配置,然后重启 Docker 服务。这类配置属于基础设施优化,实际使用中收益很明显,尤其是拉取较大镜像的时候。
第三个是平台架构的问题。在 ARM 架构的设备(比如树莓派或者某些开发板)上拉取 x86 架构的镜像会直接报错,解决办法是指定平台参数拉取多架构镜像,比如docker pull --platform linux/amd64。也可以用docker manifest inspect 镜像名查看某个镜像支持哪些平台架构,再去指定具体平台拉取。
2.2 构建镜像的两种方式
构建镜像是把应用打包成可分发镜像的过程,最常用的是docker build配合 Dockerfile。一个简单的构建命令长这样:
docker build -t my-service:v1.0 .这里-t指定镜像名称和标签,.指定构建上下文路径。构建上下文指的是 Docker 引擎在构建过程中能访问到的目录范围,指定.就是把当前目录全部打包发送给 Docker 引擎。如果目录里有依赖包、临时文件、node_modules 这类大文件夹,构建过程会非常慢,所以一定要配置.dockerignore文件,把不需要的文件排除在外。一个典型的.dockerignore长这样:
node_modules dist .git *.log .temp我以前部署某个 Node.js 服务时,没写.dockerignore,整个 node_modules 被一起打包发送给 Docker 引擎,每次构建都要等上几分钟。加上之后,构建时间直接从五分钟降到几十秒。
另一种构建方式是docker commit从现有容器生成镜像。这种方式适合快速保存现场,但不推荐用于正式构建流程,因为它的可重复性差——第一次手动配置完环境,第二次可能就忘了执行某个步骤,生成的镜像也不可追溯。我更推荐把 commit 当作一种“备份某个容器状态”的手段,而不是日常构建方案。
2.3 查看和清理镜像
镜像查看用docker images或docker image ls,加-a可以显示所有镜像(包括中间层镜像),加--no-trunc显示完整镜像 ID。查看某个镜像的详细信息可以用docker inspect 镜像名,输出 JSON 格式的数据,包含镜像的历史层、环境变量、入口命令等。
清理镜像前先要看哪些容器在用它。直接docker image rm一个正在被使用的镜像会报错,提示冲突。正确的做法是先停掉并删除相关容器,再删除镜像。也可以强制删除docker image rm -f,但我很少用强制删除,因为有可能误删被其他容器依赖的镜像层。
真正日常清理镜像推荐用docker image prune,这个命令会删除所有未被打上标签且没有被容器引用的悬空镜像,也就是<none>镜像。加-a会进一步清理所有未被使用的镜像,但要注意,它会把那些虽然没用但你可能接下来要用的镜像也删掉,执行前建议先看一遍列表再确认。
3. 容器相关的命令:完整生命周期管理
3.1 docker run 启动容器时的参数组合
容器生命周期中最重要的命令就是docker run,它负责创建并启动容器。这个命令的参数非常多,但实际用下来,日常开发中大概只需要重点掌握下面这些。
基础参数--name给容器起名字,-d让容器在后台运行,-it保持交互界面并分配终端。端口映射用-p,宿主机端口和容器端口映射的格式是-p 宿主机端口:容器端口,比如-p 8080:80表示把宿主机的 8080 端口映射到容器的 80 端口。不写-p的话容器端口在外面访问不到,这是新手最容易踩的坑——容器确实在运行,日志也没问题,但就是访问不了,最后发现端口没映射出来。
数据卷挂载用-v或--mount,两种写法效果类似。比如-v /host/data:/container/data把宿主机的目录挂载到容器目录。环境变量用-e或者--env-file,比如-e DB_HOST=10.0.0.1或者用一个.env文件一次传入多个变量。自动重启策略用--restart,设置为unless-stopped表示容器意外退出时自动重启,除非手动停止。这个参数在部署服务时几乎必加,否则宿主机重启后容器可能不会自动恢复。
一个典型的部署命令大概长这样:
docker run -d \ --name web-service \ -p 8080:80 \ -v /var/www/html:/usr/share/nginx/html \ -e TZ=Asia/Shanghai \ --restart unless-stopped \ nginx:1.27.3需要特别提醒的是-it和-d不能混用。-it表示交互模式,适合在前台运行容器并进入终端,-d表示后台运行,加了-it后-d可能不会生效,或者会出现“容器退出”的情况。
3.2 容器的启停、状态和删除
容器启动后,可以用docker ps查看运行状态,docker stop 容器名停止容器,docker start 容器名启动一个已停止的容器,docker restart 容器名重启容器。停止容器是发送 SIGTERM 信号给容器主进程,等待一段时间后(默认 10 秒)再发送 SIGKILL 强制终止。如果容器的应用需要优雅退出,比如先把请求处理完再关闭,可以把宽限时间调大或减小:
docker stop -t 30 容器名查看容器详细信息用docker inspect 容器名,返回 JSON 格式数据,里面有容器的网络配置、挂载信息、环境变量、启动命令等。用docker logs查看日志,docker stats查看资源占用情况,docker top 容器名查看容器内进程列表。
容器删除用docker container rm 容器名,默认只能删除已停止的容器,运行中的容器需要加-f强制删除。如果容器还挂着数据卷,加-v可以一并删除匿名数据卷。批量删除可以组合命令,比如把docker ps -a -q的输出传给删除命令,就能清理所有已停止的容器。
3.3 进入容器操作的两个命令:exec 和 attach
我见过很多人在进入容器这一步卡住。进入正在运行的容器主要有两个命令:docker exec -it 容器名 bash和docker attach 容器名。两者的区别很关键。
docker exec -it是在容器内启动一个新的进程,最常用的是启动一个 bash shell。这种方式不会影响容器的主进程,退出容器时直接输入exit或者 Ctrl+D 就可以了,安全无副作用。docker attach则是把当前终端连接到容器的标准输入输出上,看到的是容器主进程的输出,退出时如果按 Ctrl+C 或拨打exit,主进程可能直接退出并导致容器停止。我在一次线上排查时就是在 attach 状态下按了 Ctrl+C,结果把主进程直接杀掉了,容器瞬间退出。
所以我的建议是:日常进入容器调试一律用exec -it,除非你明确知道 attach 的退出方式且不会对主进程造成影响。另外,很多精简镜像里连 bash 都没有,比如某些 Alpine 镜像,这时候要改用sh进入:docker exec -it 容器名 sh。
4. 网络与数据卷:容器通信和数据持久化的关键命令
4.1 网络模式的选择与自定义网络
容器默认参与一个 bridge 网络,在这个网络内的容器可以通过容器名互相访问。这里有一个很实用的技巧:同一自定义网络下,容器之间用容器名就能互相解析 IP,不需要去查具体的 IP 地址。如果有一个应用容器依赖数据库容器,创建时指定同一个网络就可以直接用服务名访问:
docker network create my-net docker run -d --name db-server --network my-net mysql:8.0 docker run -d --name app-server --network my-net my-app:v1.0这样 app-server 里面的数据库连接地址直接写db-server:3306就能连通。如果两个容器不在同一个网络里,就必须通过宿主机 IP 加映射端口来互相访问,还要处理端口冲突和防火墙问题,麻烦很多。
host网络模式让容器直接使用宿主机的网络栈,没有端口映射的过程,性能开销更低,但会占用宿主机端口。如果你启动容器时报端口冲突,一是要考虑是不是有其他进程占用了,二是要检查是不是网络模式设置不当。还有none模式完全不配置网络,很少用。
查看网络列表用docker network ls,查看网络的详细配置和容器连接情况用docker network inspect,容器加入某个网络用docker network connect,退出用docker network disconnect。日志里出现“找不到容器名对应的地址”时,基本都是网络没配对或者容器不在同一网络导致的。
4.2 数据卷挂载的三种方式和权限问题
容器本身是临时性的,删除容器后容器内的数据就没了。要把数据持久化,就需要用到数据卷。数据卷有三种主要的挂载方式:匿名卷、命名卷和绑定挂载。
匿名卷是在容器启动时自动创建的卷,比如docker run -v /var/lib/mysql mysql:8.0,这种卷由 Docker 管理,但删除容器时不会自动删除,需要手动清理。命名卷在匿名卷的基础上指定了卷名:docker run -v mysql-data:/var/lib/mysql mysql:8.0,多个容器可以共享同一个命名卷,推荐用于数据库这类需要独立数据管理的场景。绑定挂载是把宿主机的具体目录映射进容器:docker run -v /host/path:/container/path,适合开发环境热更新代码或者在宿主机上直接查看日志文件的情况。
这里最容易出问题的是文件权限。容器的进程默认以 root 身份运行,挂载数据卷写入文件时,宿主机上生成的文件所有者是 root,普通用户就没有权限修改。解决方式之一是启动容器时指定用户和用户组,比如docker run --user $(id -u):$(id -g) ...,让容器进程以当前用户的身份运行,写入的文件权限就是当前用户的。我之前在一个项目里遇到数据库容器挂载目录权限不对,导致容器启动失败,排查了半天才发现是宿主机目录的属主属组没有设置正确,chown 之后就好了。
查看数据卷列表用docker volume ls,查看某个卷的信息用docker volume inspect,清理不再使用的卷用docker volume prune。
5. 排查问题的五类高频命令:日志、详情、资源、进程、事件
5.1 docker logs 看日志的几种方式
容器日志排查是日常运维的高频操作。docker logs 容器名输出容器的全部日志,-f参数可以跟随日志输出,类似tail -f的效果。--tail 100显示最近 100 行,--since "2024-06-01T08:00:00"显示指定时间之后的日志。时间可以配合时区使用,如果容器的TZ环境变量没设置成中国时区,日志时间会差 8 小时,排查问题时容易被误导,所以启动容器时我一般都会加上-e TZ=Asia/Shanghai。
有一个坑是:如果容器日志量非常大,直接docker logs可能会卡住或者把磁盘打满。Docker 默认的容器日志驱动是 json-file,日志文件会一直增长,没有自动轮转。为了避免磁盘被撑爆,推荐在daemon.json里配置日志轮转:
{ "log-driver": "json-file", "log-opts": { "max-size": "20m", "max-file": "5" } }这样每个容器最多保留 5 个 20MB 的日志文件,超过就轮转。我在部署生产镜像前都会确认这个配置已经生效,否则遇到打印疯狂的进程,几天就能写满磁盘。还有一种是直接用--log-opt在启动时指定,也可以达到相同的效果。
5.2 查看容器详情和资源占用
docker inspect是排查问题的“瑞士军刀”,它能输出容器、镜像、网络、数据卷的完整 JSON 详情。当你想知道容器的 IP 地址时,可以用docker inspect 容器名 | grep IPAddress;想确认挂载的数据卷位置时,可以用docker inspect 容器名看 Mounts 字段。也可以用 Go 模板语法过滤字段,比如只输出 IP 地址:
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 容器名资源占用情况用docker stats查看,它会实时显示每个容器的 CPU、内存、网络 I/O 和磁盘 I/O。这个命令对定位“哪个容器吃了特别多内存”很直观。不加参数时显示所有正在运行的容器,可以按名称过滤。docker stats的数值是实时变化的,不需要加刷新参数,它会持续刷新。
健壮性较差的容器往往会因为内存被耗尽而被内核 OOM Kill,这个时候docker stats可能看到内存占用一直在接近上限。要确认是否发生 OOM,可以查看宿主机日志,或者docker inspect中的 State.OOMKilled 字段是否为 true。
5.3 查看容器内的进程以及文件
docker top 容器名可以列出容器内运行的所有进程,类似宿主机上的ps命令。这个命令在对比容器资源占用和宿主机 PID 关系时特别好用,能直接看到某个进程的 PID 和启动命令。
docker cp则负责在宿主机和容器之间拷贝文件。比如想要从容器里拉取一个配置文件出来:docker cp 容器名:/etc/nginx/nginx.conf ./nginx.conf,反向也可以把宿主机文件传进去:docker cp ./app.jar 容器名:/app/app.jar。这个方法在容器里缺了某个调试工具时很有用,直接把本地的静态二进制或脚本拷贝进去就能用。
5.4 查看 Docker 事件
docker events命令可以实时输出 Docker 守护进程产生的事件,比如容器创建、启动、停止、销毁等。排查“某个容器为什么突然退出”这类问题时,这个命令很好使。在容器退出之前启动docker events --since 时间戳 --until 时间戳,能看到容器当时的退出事件和对应的错误信息。这些信息和docker logs配合起来,基本能覆盖大多数运行时异常定位场景。
6. 日常高频故障排查速查表
6.1 端口被占用导致容器启动失败
启动容器报错“port is already allocated”是最常见的启动失败原因。排查步骤是先看哪个进程占用了端口:lsof -i:8080或者netstat -tlnp | grep 8080,找到进程后决定是杀进程还是换端口。如果是 Docker 其他容器占用了,可以用docker ps查看所有容器的端口映射,找到占用者后停止或修改映射。
有时候会发现宿主机端口没被占用,但容器启动仍然报端口分配失败,这种情况多半是 Docker 在管理 NAT 规则时出了残留,重启 Docker 服务通常能解决:
systemctl restart docker6.2 容器启动后立刻退出
容器秒退是一个非常典型的问题。先查看日志:docker logs 容器名,日志里有明确的错误信息(比如“无法连接数据库”“文件不存在”“端口已占用”等),根据错误信息修配置。如果日志没有输出,大概率是应用的启动命令有问题,用docker inspect 容器名查看 Entrypoint 和 Cmd 字段,确认启动命令是不是写错了。
还有一种是容器主进程因为前台/后台模式不匹配导致秒退。有些镜像默认的命令会以前台方式运行,如果你给应用传入了类似daemon off的参数没有对应支持,进程会立即退出。另外,-it和-d混用也可能导致这种情况发生。给容器加上--restart unless-stopped是一种兜底策略,但真正解决问题还是要找到秒退的原因。
6.3 看到“IMAGE”列是<none>是什么情况
执行docker images时经常能看到仓库列和标签列都是<none>的镜像。这些是悬空镜像,通常是构建新镜像时旧版本镜像没有被打标签,或者从 Dockerfile 构建时中间的层被遗留下来了。悬空镜像不影响已有容器的运行,但会占用磁盘空间。清理命令是:
docker image prune悬空镜像不是 bug,而是 Docker 镜像分层机制下的正常产物。定期清理是有必要的,但不建议在构建过程中频繁清理,否则会影响构建缓存,导致每次构建都要重新拉取基础镜像和重新执行层操作,反而更慢。
6.4 容器时间不对的问题
很多容器默认的时区是 UTC,导致应用打印日志时间和宿主机的东八区时间差了 8 小时。处理时区问题有两种方式:一是启动时加环境变量:docker run -e TZ=Asia/Shanghai ...;二是把宿主机的时区文件挂载进去:-v /etc/localtime:/etc/localtime:ro。第二种方式在一些免交互和精简镜像上更可靠。如果两种方法都失效,就需要进容器手动配置时区或者重新构建包含时区配置的镜像。
6.5 磁盘空间被 Docker 占满
磁盘占用过高时先用这个命令看各对象占用多少空间:
docker system df它会列出镜像、容器、数据卷、构建缓存这四类分别占用的磁盘空间。清理时按优先级操作:
# 清理停机容器 docker container prune # 清理悬空镜像 docker image prune # 清理未使用的数据卷 docker volume prune # 一键清理全部未使用资源 docker system prune -a -f我用docker system df排查过几次服务器磁盘告警,最后基本都是日志文件或悬空的镜像层占了大头。生产环境清理前先确认没有正在运行的容器依赖对应的镜像或卷,prune虽然默认不会清理在用资源,但还是要保持谨慎。
7. 额外补充:写 Dockerfile 时用到的命令逻辑
虽然这篇主要讲命令,但Dockerfile里的指令和日常用到的 Docker 命令有很强的对应关系。FROM等同于拉取基础镜像,RUN等同于在容器内执行命令,COPY和ADD等同于docker cp的可重复方式,CMD和ENTRYPOINT等同于启动容器时执行的主进程命令。
这里最容易出问题是CMD和ENTRYPOINT的组合关系。CMD会被docker run后追加的命令覆盖,比如 Dockerfile 里写CMD ["nginx"],执行docker run image nginx -g "daemon off;"会把 CMD 替换成新的命令。ENTRYPOINT则固定不变,CMD变更为传给ENTRYPOINT的参数。把这个关系理解了,遇到“容器启动后执行了错误的命令”这类问题时就不会无从下手。
构建镜像时常用到的两个命令也值得提一下:docker history 镜像名可以查看镜像的构建历史,每一条对应一个层,可以看到每个层执行过什么指令,排查镜像体积突然变大的问题时很好用;docker image inspect配合--format '{{.Size}}'可以查看某层的大小变化。多层构建时每执行一个 RUN 就会生成一个镜像层,层级越多镜像越大,所以写 Dockerfile 时尽量把多个 RUN 合并为一个,再用构建缓存优化构建速度。
8. 写在最后:几个实用的日常习惯
根据我自己的使用经验,维护 Docker 环境最重要的不是记住所有命令的每个参数,而是养成几个习惯。
第一个是给容器和镜像都用有意义的名称和标签。启动容器时带上--name参数,不要依赖自动生成的随机名字,构建镜像时也不要清一色打latest标签。时间久了你会感谢自己当初顺手做的命名。
第二个是控制日志和镜像的体积。日志轮转配置上之后基本一劳永逸,但镜像体积需要持续关注。同一个项目迭代几个月后,Dockerfile 里可能堆积了一堆不再需要的安装包和中间文件,定期审视并重新构建镜像,体积往往能缩小不少。
第三个是定期查看容器状态。我习惯每天上班第一件事就是用docker ps -a看一眼环境里所有容器的状态,哪个容器昨天晚上退出了一眼就能看到。再结合docker stats看资源占用,大部分轻微问题都能在变成事故之前提前发现。
第四个是别怕删掉搞坏了的容器。很多新手因为怕数据丢失,容器出了问题就慌,不敢删除。其实容器层面可以通过数据卷和镜像恢复,数据卷才是真正需要保护的东西。搞清楚哪些数据必须持久化在卷里、哪些随时可以重建,操作时就会更有底气。
回到开头那个问题,Docker 命令本质上是在和四类对象打交道:镜像、容器、网络、数据卷。把每个命令归到对应的对象上,再结合运行状态去理解,就不会再出现“明明启动了容器却访问不到服务”这类让人哭笑不得的困惑了。如果你现在正在学 Docker,把这篇里的命令挨个在本地环境跑一遍,比单纯记参数背命令要管用得多。