Docker 入门系列写到第二章,说明你已经把环境装好、第一个 hello-world 跑起来了。这一篇没有什么玄乎的理论,核心就是把命令练熟。镜像怎么拉、容器怎么跑、数据怎么留、日志怎么看、没用的东西怎么清,这几件事搞明白,日常开发和个人项目就基本够用了。
我把 Docker 常用命令分成六块:镜像管理、容器生命周期、日常运维、数据卷与端口、镜像搬运、清理与资源查看。每一块都会先说它解决什么问题,再给命令和参数,最后补上我实际踩过的坑。这一章按照“先镜像、后容器、再数据、最后清理”的顺序写,和官方文档思路一致,跟着走一遍,命令记忆会顺很多。
1. 镜像管理:先搞清楚你手里有什么
1.1 镜像和容器的关系,别急着敲命令
很多新手第一次接触 Docker,上来就 docker run,跑起来之后又忘了镜像和容器到底谁是谁。用一个生活类比:镜像就是安装光盘 + 系统文件,容器就是用这张光盘启动起来的一个实例。同一张光盘可以启动多个实例,它们彼此隔离;光盘本身不变,实例里改了什么东西也不会影响光盘。
这个区别非常重要,因为后续所有命令都绕不开这两个对象。docker images 看的是“光盘”,docker ps 看的是“正在运行的实例”。如果你把容器删了,镜像还在;如果把镜像删了,正在运行的容器通常不受影响(因为容器已经跑起来了),但容器停止后可能无法再启动,因为它依赖的镜像层被清理了。我的习惯是:先查镜像,再决定是否运行容器;先查容器,再决定是否清理镜像。
1.2 拉镜像和看镜像:pull、images、inspect
拉镜像用 docker pull,最简单的形式是 docker pull nginx:latest。这里的 nginx 是仓库名,latest 是标签。标签不等于版本号,latest 一般代表最新稳定版的滚动指针,所以线上环境我建议指定明确版本,比如 docker pull nginx:1.25.3。这样后续别人拉下来的镜像不会因为标签漂移而出现行为差异。
拉完镜像后,用 docker images 查看本地列表。输出里有 REPOSITORY、TAG、IMAGE ID、CREATED、SIZE。其中 IMAGE ID 是镜像的唯一标识,注意它不是完整 sha256,而是前 12 位。如果同一镜像的不同标签都指向同一个 IMAGE ID,说明它们本质上是同一份镜像,只是名字不同。
docker inspect 经常被忽略,但它非常有用。docker inspect nginx:1.25.3 会输出一大段 JSON,包含镜像的架构、暴露端口、环境变量、默认命令等信息。如果你在写 Dockerfile 或者排查镜像来源,这个命令比翻网页快得多。嫌输出太长可以加过滤参数,比如 docker inspect -f '{{.Architecture}}' nginx:1.25.3,只拿架构字段。
镜像管理这里还有一个高频场景:搜索镜像。docker search nginx 可以搜官方仓库里的镜像,但实际使用中我几乎不用它,因为在网页上能看到更详细的下载量、是否官方维护、是否支持多架构。如果你在命令行里搜索,重点关注 OFFICIAL 列的 OK,优先选官方镜像,避免下载来路不明的镜像。
2. 容器生命周期:run 是起点,但不是终点
2.1 run 命令的标准姿势与常用参数
docker run 是出现频率最高的命令,也是最容易记混参数的命令。一个标准示例是:
docker run -d --name web -p 8080:80 -v /opt/html:/usr/share/nginx/html nginx:1.25.3这条命令做了四件事:-d 表示后台运行,--name 给容器起名,-p 做端口映射,-v 挂载数据卷。先说 -d,不加这个参数容器会以前台模式运行,日志会直接打在终端上,Ctrl+C 容器就停了。调试的时候前台模式方便,正式使用基本都要 -d。
--name 建议每次必加,不加的话 Docker 会随机生成一个名字,比如 quirky_hoover。后续查看日志、进入容器、删除容器时,没有名字就得靠容器 ID,而 ID 很难记。用名字之后操作语义清晰得多。
-p 参数在第四部分详细讲,这里先记一个格式:-p 宿主机端口:容器端口。-v 同理。run 命令还有一个容易忽略的参数是 --rm,它表示容器停止后自动删除。临时跑一次测试任务时,加上 --rm 可以避免残留一堆停止状态的容器。
第一次运行一个新镜像时,如果本地不存在,docker run 会自动执行 pull,所以你可以直接写 docker run nginx,省略显式 pull。但这会掩盖镜像下载过程,如果下载失败,报错信息会混在一起。我的习惯是:先 docker pull,再 docker run,分两步走,日志清晰,排查问题也方便。
2.2 容器状态切换:start、stop、restart、rm
run 创建并启动一个容器,之后容器的生命周期就交给下面这些命令:
docker start web # 启动已存在的容器 docker stop web # 优雅停止,给容器一定时间处理收尾 docker restart web # 重启,相当于 stop + start docker rm web # 删除容器stop 和 kill 的区别值得单独说。docker stop 会先发送 SIGTERM 信号,让容器里的主进程有时间保存状态、清理资源,默认等待 10 秒;如果超时未退出,再发送 SIGKILL 强杀。docker kill 则是直接发送 SIGKILL,不留情面。日常场景优先用 stop,只有确认容器卡死时才用 kill。10 秒等待时间可以通过 -t 参数调整,比如 docker stop -t 30 web。
rm 删除容器时有个常见报错:Error response from daemon: cannot remove container ... you cannot remove a running container。必须先停止容器再删除。如果你想一步到位,用 docker rm -f web 会强制删除运行中的容器,相当于先 kill 再删。开发环境图省事可以这么干,生产环境建议按顺序来,避免误删。
还有一个命令 docker create,它和 run 的关系是:create 只创建容器但不启动,run 等价于 create + start。这个命令实际用得少,但在编排复杂启动参数时很有用,可以先 create 并拿到容器 ID,确认参数没有问题再 start。
3. 日志、进程、进入容器:日常运维三件套
3.1 看日志:logs 的参数和实战用法
容器跑起来之后,第一件事就是看日志。docker logs web 会输出容器标准输出和标准错误的所有内容。如果程序日志是输出到文件而不是 stdout/stderr,默认是看不到的,这点很多新手踩坑。比如某些 Java 应用倾向于直接写文件日志,Docker 容器里最好把日志重定向到标准输出,否则 docker logs 是空的。
logs 常用参数有几个:
- docker logs -f web:follow 模式,实时跟踪日志输出,类似 tail -f。
- docker logs --tail 100 web:只显示最后 100 行。
- docker logs --since 2024-01-01T00:00:00 web:显示指定时间之后的日志。
- docker logs -t web:每行日志前面加时间戳。
--since 配合时间字符串或相对时间都行,比如 --since 10m 表示最近 10 分钟。排查故障时我一般这么组合:
docker logs --tail 200 -t --since 30m web这能快速判断容器在最近半小时内有没有异常输出。如果日志量特别大,docker logs 可能会一次性输出很多内容,建议先用 --tail 限制行数,而不是直接裸跑。
3.2 进入容器:exec 和 attach 的区别
要进入运行中的容器,最常用的是 docker exec。它的典型用法是:
docker exec -it web bash这里 -i 表示交互模式,-t 表示分配一个终端。如果容器里没有 bash,就尝试 sh,因为很多精简镜像只带 sh。比如官方 alpine 系列镜像,默认没有 bash,直接 docker exec -it alpine bash 会报错,换成 sh 就能进。
还有一种进入方式是 docker attach web,它会把当前终端连接到容器的标准输入输出。但注意 attach 和 exec 有本质区别:attach 是接入容器的主进程,在主进程退出时,终端会断开,而且如果你按 Ctrl+C,可能会直接把容器主进程干掉。exec 是额外启动一个新进程,不影响主进程。日常操作我只用 exec,attach 只在调试特殊容器或者需要查看交互界面时才用。
在容器里修改文件要谨慎。比如用 exec 进入容器改了 nginx 配置,如果容器重新创建,所有改动都会丢失。这种场景的正确做法是用数据卷挂载配置文件,或者把改动写进镜像。我自己刚开始用 Docker 时,直接进容器改配置,结果容器一重启,改动全没了,后来才意识到容器是不保存运行时修改的。
3.3 查进程和资源占用
docker top web 可以查看容器里的进程列表,类似宿主机上的 ps 命令。它会显示容器内所有进程的 PID、PPID、运行用户和命令。当容器卡死或 CPU 异常飙升时,用这个命令快速定位是哪个进程在捣乱。
更直观的资源监控用 docker stats。这个命令会输出一个实时刷新的表格,包含每个运行容器的 CPU、内存、网络 I/O 和磁盘 I/O。不加参数时默认监控所有运行中的容器。注意这条命令不会显示已停止的容器。docker stats 对排查内存泄漏和 CPU 异常非常有用,但它有轻微的性能开销,生产环境不建议一直开着,排查完就退出。
4. 数据卷与端口映射:让容器不再是“一次性”
4.1 挂载数据卷:-v 和 --mount
容器本身是临时的,但数据不能丢,所以数据卷一直是 Docker 使用中的核心概念。最简单的挂载方式就是 -v:
docker run -d --name web -v /opt/html:/usr/share/nginx/html nginx:1.25.3这个命令把宿主机的 /opt/html 目录挂载到容器的 /usr/share/nginx/html 目录。宿主机目录里放什么,容器里就能看到什么;容器里写入文件,宿主机也能看到。这样即使容器删了,数据也留在宿主机上。
-v 参数还有一种匿名卷的用法:-v /container/path。这时 Docker 会创建一个由 Docker 管理的卷,在某个随机位置存放数据。这个位置通过 docker inspect 可以查到,但不好管理,所以匿名卷通常用于容器内部产生的临时数据。
更规范的写法是 --mount:
docker run -d --name web --mount type=bind,src=/opt/html,dst=/usr/share/nginx/html nginx:1.25.3--mount 的语法更明确,type 支持 bind 和 volume。bind 是绑定宿主机目录,volume 是使用 Docker 管理的卷。新版 Docker 推荐优先使用卷,因为卷有更好的备份、迁移和权限管理机制。个人小项目用 -v 绑定宿主机路径最方便,正式环境建议考虑 named volume。
还有一个容易踩的坑:挂载目录的权限。如果宿主机目录权限是 root,而容器里进程以普通用户运行,写入文件时会报 Permission denied。解决方法是把宿主机目录权限放宽,或者在 run 时用 --user 指定运行用户。记得给 nginx 容器挂载目录时,宿主机目录的属主和容器内 nginx 进程的用户要匹配,否则页面直接 403。
4.2 端口映射:-p 和 -P
容器有自己独立的网络栈,外部网络默认访问不到容器内的服务,必须做端口映射。-p 8080:80 的意思是:访问宿主机的 8080 端口,流量转发到容器的 80 端口。这个 80 端口是容器内进程监听的端口,不是随便写的。
常见几种映射写法:
- -p 8080:80:指定宿主机端口和容器端口
- -p 127.0.0.1:8080:80:只监听本机回环地址,外部网络无法访问
- -p 80:随机分配一个宿主机端口,映射到容器 80
- -P:把所有通过 EXPOSE 声明的端口都映射到随机宿主机端口
最后两种随机映射很少用在正式环境,因为它们会导致端口不确定,不利于服务发现。但调试时可以快速验证服务是否正常,用 docker port 命令查看随机映射结果:
docker port web输出类似 80/tcp -> 0.0.0.0:32768,表示容器的 80 端口映射到了宿主机的 32768。在写启动脚本或跨容器通信时,尽量显式使用固定端口,否则重新创建容器端口可能变化。
Docker 容器间通信还有一个更优雅的方案:自定义网络。用 docker network create 创建一个网络,然后把多个容器加入同一个网络,它们之间可以直接用容器名互相访问,不需要映射到宿主机。这个机制在跑前后端联调时特别顺手,比如后端容器内可以直接通过 http://mysql:3306 访问数据库容器。具体网络命令会在后面的章节单独展开,这里先记住 -p 解决的是“外部访问容器”的问题,容器间互访优先走自定义网络。
5. 镜像搬运:tag、commit、save、load
5.1 给镜像打标签:docker tag
docker tag 的作用是给镜像添加一个别名或版本标签。比如:
docker tag nginx:1.25.3 myregistry.example.com/nginx:1.25.3这个命令不会复制镜像内容,只是在镜像 ID 上多挂一个名字。后续 push 到私有仓库时,镜像名要带上仓库地址,如果不先 tag,默认会推到公共仓库,容易出问题。我的习惯是:所有推送私有仓库的镜像,先 tag 再 push,标签格式统一为 仓库地址/命名空间/镜像名:版本号。
tag 还有一个用途:日常开发时给镜像打一个“当前版本”的标记,避免 latest 混乱。比如构建完成后 docker tag web:latest web:20240601,然后用固定的日期版本去部署,回滚时也能快速定位。
5.2 提交容器快照:docker commit
docker commit 可以把一个容器的当前状态保存成新镜像。用法:
docker commit web web-backup:20240601这相当于把容器里所有文件系统的改动打包成新镜像。听起来很方便,但我不推荐靠 commit 做正式镜像。原因有几个:第一,commit 会把容器运行过程中产生的临时文件、日志、缓存一并打包,镜像会变得臃肿;第二,commit 丢失了构建层级信息,别人拿到这个镜像完全看不出它基于什么基础镜像、做了什么操作;第三,运行时的进程、端口映射、环境变量不会保留在 commit 的镜像里。
真正规范的做法是写 Dockerfile,用 FROM、RUN、COPY 等指令构建镜像。commit 只适合抢救场景,比如容器里已经手动配置好一套环境,来不及写 Dockerfile,先 commit 保住现场。提交之后还是要尽快把 Dockerfile 补上。
5.3 导出和导入:save、load
把镜像保存成 tar 文件,可以用 docker save:
docker save -o nginx.tar nginx:1.25.3在另一台机器上导入用 docker load:
docker load -i nginx.tarsave 和 load 适合离线环境迁移镜像。save 后的 tar 文件不带压缩,体积可能很大,可以用 gzip 压缩:
docker save nginx:1.25.3 | gzip > nginx.tar.gz加载时注意 docker load 只能导入镜像,不能导入容器。如果想迁移容器,标准做法是先把容器 commit 成镜像,再 save 镜像。另一个容易混淆的导出命令是 docker export,它导出的是容器的文件系统,而不是镜像,不包含镜像的元数据和分层信息。新手经常把 export 和 save 搞混,记住一句话:save 管镜像,export 管容器。
6. 清理与资源查看:磁盘空间救星
6.1 查看磁盘占用:docker system df
Docker 用久了,磁盘空间会悄悄被镜像、容器、数据卷占满。docker system df 可以一条命令看到总占用:
docker system df输出包含镜像、容器、本地卷、构建缓存四类的总大小和可回收大小。TYPE 列为 images、containers、local volumes、build cache,RECLAIMABLE 表示可以清理的部分。每次清理前先跑这条命令,心里有个底,不然盲目清理可能把你的数据卷删掉。
6.2 清理无用的对象:docker container prune、image prune、volume prune
清理停止状态的容器:
docker container prune加上 -f 可以跳过交互确认。默认只会清理停止状态的容器,运行中的容器不受影响。如果想连停止的容器带网络都清理,可以用 docker system prune,它会把所有未使用的镜像、容器、网络、构建缓存一并清掉,但不会清数据卷。这一步我想强调:带不带 -a 差别很大。
docker system prune 默认只清理“悬空”镜像,也就是没有标签、且没有被任何容器引用的镜像。docker system prune -a 会清理所有未被容器使用的镜像,包括你可能想保留的历史版本。所以我在做全量清理之前,都会先 docker images 看一眼,确认没有需要保留的旧镜像。
清理数据卷要额外小心:
docker volume prune这个命令会删除所有未使用的数据卷。数据卷不像镜像能重新拉取,删了很难找回。如果你有一个保存数据库文件的卷,容器停了但卷里还有数据,执行 volume prune 前一定要检查是否还有未挂载的卷需要保留。我的习惯是:数据库容器除非特殊原因,否则数据卷的清理都是手工执行 docker volume rm,不接受批量 prune。
6.3 查看所有容器:ps 与 ps -a
docker ps 只显示运行中的容器,docker ps -a 显示所有容器,包括停止状态的。排查问题时我几乎都是先 ps -a,因为停止的容器往往残留了报错线索。docker ps 输出里的 STATUS 列很有讲究:Up 表示正在运行,Exited 表示已停止,Restarting 表示正在重启中。
如果一个容器反复 Restarting,多半是启动命令有问题,比如主进程崩溃或者启动时依赖的配置文件缺失。这时先用 docker logs 看日志,如果日志是空的,再考虑是不是主进程在前台运行但立即退出了。容器启动时主进程必须在前台运行,不能用 systemd 或 nohup 把进程放到后台,否则 Docker 会认为容器无事可做直接退出。这是我见过最多的一类容器启动失败原因。
7. 常见问题与排查技巧实录
7.1 容器启动后立即退出
最常见的场景:docker run 之后容器马上变成 Exited。先用 docker ps -a 确认状态,再用 docker logs 看输出。如果是 nginx、redis 这类官方镜像,通常是配置文件或端口被占用问题。如果日志为空,检查 Dockerfile 里的 CMD 是不是前台执行业务进程。
有一个经典案例:用 docker run 跑一个简单的脚本容器,脚本执行完就退出,这在正常不过。容器不是虚拟机,主进程退出容器就结束。想让容器长期存活,要么让主进程处于阻塞状态,要么用 tail -f /dev/null 这样的技巧挂住前台进程。不过这个技巧只适合临时测试,正式服务还是要设计成前台常驻。
7.2 端口冲突:bind address already in use
启动容器时报 bind: address already in use,说明宿主机的映射端口已经被占用。先用 docker ps 看是不是其他容器占了端口,再用 ss -lntp 或 netstat -lntp 查宿主机进程占用情况。如果是不同容器冲突,改端口号重新 run;如果是其他系统进程占用,考虑换一个端口。
有时还会遇到容器内部端口冲突,比如 mysql 容器默认监听 3306,你硬要指定容器端口为 3306,但容器内部已经有进程占用了。这种通常不是 -p 的问题,而是镜像默认配置的问题,改镜像配置或使用自定义网络解决。
7.3 镜像拉取缓慢或超时
国内网络环境下直接拉官方仓库镜像经常超时,最快的方法是配置镜像加速器。这一步很多教程写得很复杂,其实就是在 Docker 配置文件里加 registry-mirrors。配置后重启 Docker 服务,再用 docker info 查看 Registry Mirrors 字段是否生效。
不同环境的配置文件路径不太一样,常见的几个路径是 /etc/docker/daemon.json、~/.docker/config.json,改对了文件就行。加速器配置只能影响 docker pull 和 docker run 时的镜像下载,不会影响容器运行时的网络。如果拉取超时,可以重试几次,也可以考虑直接用代理环境,但这属于网络规划问题,这里不多展开。
7.4 进入容器时 exec 报错
docker exec -it web bash 报错 exec: "bash": executable file not found in $PATH,几乎可以确定容器里没有 bash。换 sh 即可。如果 sh 也没有,说明镜像精简到极致,比如纯二进制镜像,那就得考虑用 Dockerfile 添加 shell 或在构建阶段就把调试工具装进去。
还有一次遇到 exec 命令卡住,原因是没有加 -it,只是 docker exec web bash,结果没有分配终端,bash 进程一直是僵死状态。进入交互式 shell 一定要加 -i 和 -t,缺一个都可能出现输入无响应或报错。
7.5 删除镜像时提示被容器引用
docker rmi 报 image is being used by running container,说明有容器正在使用这个镜像。先 docker ps 或 docker ps -a 找到引用它的容器,停止并删除容器,再删镜像。如果容器已经停止但还在列表里,也需要先 docker rm 删除容器。加上 -f 强删镜像在大多数情况下不建议,因为可能导致无镜像可用的容器无法启动。
批量删除容器或镜像时,可以用 docker rm $(docker ps -aq) 或 docker rmi $(docker images -q)。这类命令组合在清理环境时效率很高,但执行之前最好先运行子命令看看列表,避免误删。docker ps -aq 会把所有容器 ID 列出来,包括运行中容器,所以 docker rm $(docker ps -aq) 会报错说无法删除运行中的容器,需要先结合 docker stop 用。
我个人的习惯是平时尽量手动管理容器名字,不依赖批量命令。等到真的需要全量重建环境时,直接 docker system prune -a 一次清干净比逐个删除快得多。
8. 第二章收尾:命令学完,习惯更重要
写到这里,Docker 常用命令的基本盘就算过了一遍。镜像、容器、日志、数据卷、清理,每一个方向都有对应的命令,而且这些命令之间是互相关联的。docker run 是入口,logs 和 exec 是日常排查,volume 和 network 是让容器真正融入你的工作流,prune 是避免磁盘爆炸的保险丝。
我从一开始用 Docker 到现在,踩过最深的坑就是“把容器当成虚拟机”。进容器改配置、在容器里装软件、依赖容器持久保存数据,这一套传统服务器的习惯在容器世界里全都行不通。容器是快餐,吃完盘子一扔,这才是它高效的原因。所以那些需要保存的数据、需要复用的配置、需要跨环境传递的镜像,都应该用卷、Dockerfile、镜像仓库去管理,而不是依赖某个容器的“现场”。
第二章学的这些命令,几乎每个项目都会用上。建议你找一个简单的 Web 服务镜像,比如 nginx 或者一个 Python 写的极简接口,反复做几遍“创建容器、改代码、提交镜像、删除容器、再重新部署”的循环。命令本身不难,难的是把“容器不可变、数据要挂出来、环境用镜像固化”这几个意识内化成习惯。下一章可以开始碰 Dockerfile 和镜像构建,到时候你会发现在宿主机上怎么手动部署,就怎么把它转成可重复的构建脚本。