ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker生产环境高频命令实战:从镜像拉取到排障清理

Docker生产环境高频命令实战:从镜像拉取到排障清理 用了这么多年 Docker带过的团队也有几个了我发现一个很有意思的现象很多人对 Docker 命令的掌握停留在能拉镜像、能 run 起来的程度一旦镜像拉不下来、容器起不来、端口不通、磁盘被日志打爆就开始慌了。其实这些问题的根源往往不是 Docker 本身多复杂而是对常用命令背后的机制理解不够透。这篇文章就把我在生产环境里真正高频使用的 Docker 命令按功能拆开讲不是命令大全式的罗列而是每个命令解决什么问题、为什么要这么用、坑在哪里希望对正在学 Docker 或者被 Docker 折磨过的朋友有点帮助。1. 镜像拉取与标记这两个反直觉细节要先搞清楚1.1 tag 不是版本号而是指向镜像 ID 的引用很多新手会把nginx:1.27这种 tag 当成版本号习惯性地认为 tag 对应某个具体的镜像文件。这个理解在 99% 的场景下够用但它会在你手动打 tag 时带来困惑。实际上Docker 的 tag 只是指向某个镜像 ID 的可读标签一个镜像 ID 可以拥有任意多个 tagtag 可以被覆盖覆盖之后它指向新的镜像 ID。这一点和 Git 的分支引用有点像分支名也不是固定不变的它只是指向某个 commit 的可变指针。举个例子docker pull nginx:1.27 docker tag nginx:1.27 myregistry.com/nginx:1.27 docker tag nginx:1.27 myregistry.com/nginx:2024-01-release执行之后你会发现docker images里出现了两个新条目但它们的 IMAGE ID 和nginx:1.27完全一样。这三个 tag 实际共享同一份镜像数据。这里就引出了第一个实用技巧当你需要把一个镜像从测试环境推到自己的镜像仓库时docker tag加上docker push是标准操作不用重新docker build。还有一个常见误解latesttag 不等于最新版本。latest只是一个默认 tag很多镜像维护者发布新版时不一定更新latest甚至有的项目明确不维护 latest。所以生产环境拉镜像时我建议永远使用明确的版本 tag而不是 latest。1.2 悬空镜像是怎么产生的以及 dangling 过滤悬空镜像dangling image是 Docker 世界里最容易被忽略的垃圾。它产生的原因很简单当你用同样的 tag 重新 build 一次镜像或者重新 pull 了同名但版本不同的镜像旧镜像 ID 不再被任何 tag 引用就变成了none:none状态。这些悬空镜像平时不会影响你使用但会占磁盘空间而且会干扰docker images的输出让你分不清哪个镜像还在被使用。查悬空镜像的命令docker images -f danglingtrue清理它们docker image prunedocker image prune默认只清理悬空镜像不会动有 tag 的镜像。加上-a参数会把所有未被容器使用的镜像也一并清理这个要谨慎因为有可能你只是暂时没用到某个镜像删掉之后下次还得重新拉。1.3 --platform 与多架构镜像的坑这个问题我踩过一次。当时在一台 x86 服务器上直接docker run了一个 ARM 架构的 Go 服务镜像启动之后进程反复崩溃日志又没输出什么有效信息排查了很久才发现是镜像架构不匹配。如果你的镜像托管在 Docker Hub 或主流镜像仓库拉取时会自动匹配当前机器的架构。但当你手动处理一些老旧镜像或非官方仓库的镜像时就得用--platform指定架构docker pull --platform linux/arm64 golang:1.22同理构建镜像时也建议在构建阶段用docker build --platform配合多架构构建工具避免本地能跑服务器不能跑的尴尬。这里补充一个排查技巧执行docker inspect image_id后查看Architecture和Os字段能确认镜像的架构信息比上网查文档快得多。2. 容器生命周期run 时的参数决定运维阶段省不省心2.1 这些 run 参数强烈建议每条生产命令都带上很多教程教你docker run只教三个参数镜像名、端口映射、环境变量。但真实生产环境里容器是被期待长期稳定运行的不是起个进程跑一次就完事。以下几个参数是我在创建生产容器时几乎必带的每个都对应一个真实事故场景。首先是--restart unless-stopped。这个参数的作用是容器异常退出时自动重启但手动 stop 后不自动拉起。我见过不少团队没加这个参数服务器一重启几十个容器全部处于 Exited 状态服务大面积不可用运维同学一脸懵。注意always和unless-stopped的差别always即使在容器被手动停止后Docker 服务重启时也会把它拉起来这有时不是你想要的行为unless-stopped在手动停止后不会再被拉起更符合日常运维直觉。其次是资源限制。不加--memory和--cpus的容器能吃掉宿主机全部资源。这个场景很典型一个 Java 应用内存泄漏宿主机的内存从 70% 一路涨到 99%其它业务容器全部跟着遭殃最后 SSH 都登不上去了。加了限制之后顶多这个容器本身挂掉别的容器还是健康的docker run -d --name myapp \ --restart unless-stopped \ --memory1g --memory-reservation768m \ --cpus1.5 \ myapp:1.0--memory-reservation是软限制正常情况下容器会用这个值的资源内存压力大的时候才会被限制到硬上限--memory。--cpus1.5表示最多使用 1.5 个 CPU 核心。再一个是--init。容器和宿主机共享内核但容器内的 PID 1 进程如果不处理信号转发会产生一个很麻烦的问题容器内的主进程 fork 出的子进程变成孤儿进程没人回收积少成多就会成为僵尸进程。加上--init后Docker 会注入一个轻量级的 init 进程作为 PID 1负责信号转发和子进程回收。一个小参数解决一个长期隐患值得养成习惯。2.2 docker exec 的交互到底指什么docker exec -it container bash是进容器最常用的命令但很多新手分不清-i和-t分别有什么用。-i是 interactive保持标准输入打开这样你能给容器内的进程输入内容-t是 tty分配一个伪终端这样能显示颜色、支持交互式操作类似 SSH 进去的效果。如果你只是想在容器内执行一条非交互命令比如批量导出数据、查看配置就不需要加-t甚至不需要-idocker exec container cat /etc/nginx/nginx.conf docker exec container sh -c echo some text /tmp/test.txt这里要提醒一个 Shell 中容易踩的坑docker exec container echo a | cat中管道是在宿主机执行的不是容器内执行的。如果想让容器内执行管道必须显式用sh -c包一层。我在团队里见过有人在这个问题上排查了很久一直在宿主机上一遍遍试命令容器里的输出始终不对。2.3 docker stop 背后的优雅停机机制docker stop不是直接一刀砍死容器它默认发送 SIGTERM 信号给容器内 PID 1 进程等待 10 秒如果容器还没退出再发送 SIGKILL 强制终止。这 10 秒是给应用做优雅停机用的——关闭连接池、保存状态、清理临时文件。但问题来了很多应用进程本身不会处理 SIGTERM收到信号后直接退出那你配置的优雅停机逻辑根本没执行。所以说到底docker stop -t 30 container只是给了容器 30 秒的宽限期应用能不能在宽限期内完成收尾工作取决于应用自身对信号的处理能力。这里讲一个日常经验如果需要更新容器我一般先docker stop等它自己退出再docker rm而不是直接用docker rm -f强杀除非是业务真的卡死了。强杀容易留下未清理的临时文件和未持久化的数据。3. 排障三板斧日志、inspect 与 exec 的正确组合3.1 docker logs 的这些参数比你想象的能打docker logs不只是docker logs container看全部输出它支持按时间和行数过滤这是排障时的利器。比如你想看最近 30 分钟的报错docker logs --since 30m container docker logs --since 2024-06-01T10:00:00 container想看最后 200 行并持续跟踪docker logs --tail 200 -f container-f是 follow行为类似tail -f。排障的时候我建议先--tail看最近日志而不是直接全量拉取因为容器跑了很久之后全量日志可能是几十 MB 甚至上 GB直接刷到终端里终端会卡死。还有一个冷门但实用的点容器内进程把日志写到文件而不是标准输出时docker logs什么都看不到。这个不算故障是它根本没往标准输出写。处理方式docker exec container tail -n 100 /var/log/app.log如果应用日志是通过 log4j 这类库写到文件最好直接把应用配置改为输出到标准输出然后由 Docker 日志驱动统一收集这才是符合 Docker 使用习惯的做法。3.2 inspect 不是只用来看状态的配合 jq 才是完整体docker inspect能拿到容器的完整 JSON 信息包括状态、网络、挂载、环境变量、启动命令、健康检查结果等等。问题是裸输出的 JSON 又臭又长没人想盯着那几百行看。配合jq可以精准提取字段这是我日常用得最多的命令组合之一docker inspect container | jq .[0].State.Status docker inspect container | jq .[0].NetworkSettings.IPAddress docker inspect container | jq .[0].Mounts docker inspect container | jq .[0].Config.Env如果不方便装 jq也可以用 Go 模板docker inspect -f {{.State.Status}} container docker inspect -f {{range .Mounts}}{{.Source}} - {{.Destination}}{{println}}{{end}} container排查容器为什么没起来时第一个应该看的不是日志而是State和ExitCode。比如 ExitCode 为 137说明容器被 SIGKILL 杀掉了如果是内存超限大概率是 OOMKilled 字段变成了 truedocker inspect -f {{.State.OOMKilled}} container这个字段救过我一次当时容器莫名其妙重启用日志查了半天没头绪一查 OOMKilled 是 true立刻明白了是内存不够加内存限制之后解决。3.3 exec、stats、port排障链路的最后拼图当容器能启动但业务异常时我会按这个顺序排查先docker stats看资源占用再docker exec进容器看进程和网络最后docker port确认端口映射是否正常。docker stats类似宿主机的top实时显示每个容器的 CPU、内存、网络 I/O。加上--no-stream可以只取当前一帧的数据用在脚本里采集指标非常合适docker stats --no-stream --format table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}进容器后常用的排查命令是看端口监听和进程树docker exec container netstat -tulnp docker exec container ps aux docker exec container curl -I http://127.0.0.1:8080注意一点容器里未必装了 netstat 和 curl。如果打包镜像时没把这些工具打进去就得用docker exec配合cat /proc/net/tcp这种取巧方式或者在镜像构建时就把 iproute2、curl 这些排障工具装好。我自己的基础镜像里默认会装 curl、ping、net-tools排障的时候能省非常多时间。docker port container这个命令有一个隐藏用途当宿主机上端口重新映射后你可以用它快速确认容器当前实际占用的是哪个宿主机端口比去翻docker ps的输出要直观。4. 数据卷与网络最容易出错的命令集中营4.1 三种数据挂载方式选错真的会丢数据Docker 容器是用完即弃的容器删除后里面的数据也跟着没了。为了让数据持久化必须用卷挂载。Docker 提供三种挂载方式bind mount宿主机目录直接映射进容器、volumeDocker 管理的卷、tmpfs仅存在于内存中。我见过最惨痛的一个事故有人直接用docker run -d mysql:8.0跑了一个 MySQL完全没挂载数据目录。后来容器因为日志满了被清理整个数据库瞬间消失连备份都没来得及做。这就是没理解容器是抛弃式的数据必须挂载出来的典型教训。正确姿势单机最常用 bind mount因为你能直接在宿主机上看到文件也方便备份docker run -d --name mysql \ -v /data/mysql:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORDyour_password \ mysql:8.0注意-v和--mount的区别。-v语法简单但自动帮你创建目录时权限不可控--mount是显式声明更安全适合生产环境docker run -d --name mysql \ --mount typebind,source/data/mysql,target/var/lib/mysql \ mysql:8.0volume则适合不想关心数据实际存储位置、只需要 Docker 自己管理的情况。列一下本机的卷docker volume ls docker volume inspect volume_name这里有个容易忽略的场景docker run -v /var/lib/mysql不指定宿主机路径时Docker 会自动创建一个匿名卷。容器删除后用docker volume ls能看到一堆无名的 volume这些可能就是历史遗留的数据占着磁盘空间——可以说这是数据还在但你不知道它属于谁的隐患所以建议要么用指定路径的 bind mount要么用带名字的docker volume create mydb-data。4.2 自定义网络和容器间 DNS 解析默认情况下容器跑在bridge网络里容器之间可以通过 IP 互通但 IP 是动态的。如果你容器 A 要访问容器 B用 IP 地址写配置容器 B 重建后 IP 变了A 就访问不到了。正确做法是创建自定义 bridge 网络Docker 会在自定义网络里自动做基于容器名的 DNS 解析docker network create mynet docker run -d --name app1 --network mynet myapp:1.0 docker run -d --name app2 --network mynet myapp:1.0这样在 app1 容器里直接curl http://app2:8080就能通容器重建后 IP 变化也不影响。这个特性真的是自建 Spring Cloud 或普通 MySQL/Redis 容器互访时的救命稻草。查看网络和执行网络操作的基础命令docker network ls docker network inspect mynet docker network connect mynet app1 docker network disconnect mynet app1再提示一个排查思路如果两个容器明明分配在不同网络里互相 ping 不通不要先怀疑防火墙先看一下它们是否在同一个 user-defined bridge 里。跨网络互访不是默认行为。4.3 端口映射的几个容易误判的细节docker run -p 8080:80 nginx的意思是宿主机的 8080 端口映射到容器的 80 端口。有几个细节容易误判一是映射范围是宿主机 0.0.0.0 地址。如果你不希望外部网络访问服务只希望本机能访问可以绑到回环地址docker run -d -p 127.0.0.1:8080:80 nginx这样宿主机之外的人无法访问这个端口对调试本地服务非常有用。二是容器内的端口改变不会自动反映到宿主机映射关系上。如果你把容器内服务从 80 改成 8080但没重新调整-p 8080:80宿主机 8080 上依然映射的是容器 80 端口你访问会失败。排查时建议先docker port container看当前实际映射关系。三是宿主机端口被占用时docker run会直接报错「port is already allocated」。处理方式无非是换端口或者docker rm -f掉占用容器的进程但千万别忽略docker ps -a里那些已经退出的容器——它们仍然可能占用端口映射。5. 磁盘与日志容器跑久了必会的清理组合5.1 用 system df 和 prune 把磁盘空间找回来运行一段时间后很多人的宿主机磁盘会莫名奇妙地满一查罪魁祸首经常是 Docker镜像、容器、卷、构建缓存各有各的占用。不要凭感觉瞎删先看官方统计docker system df这个命令会按类型列出镜像、容器、本地卷、构建缓存的磁盘占用情况。看到RECLAIMABLE一栏的数字你就可以对症下药。清理命令有不同力度docker image prune # 清理悬空镜像 docker container prune # 清理所有已停止的容器 docker volume prune # 清理未使用的本地卷慎用 docker builder prune # 清理构建缓存 docker system prune -a # 以上全部清理docker system prune -a是最狠的它会删掉所有未使用的镜像和缓存、已停止的容器等。执行前建议先运行不带-a的docker system prune看它列出哪些东西确认不会误删再做。5.2 日志无限膨胀一个必须提前配置的参数默认情况下 Docker 使用json-file日志驱动日志文件会无限增长。这个坑我印象太深了有一个容器每天产生 2GB 日志宿主机磁盘 40GB跑两周直接满了服务全部挂掉排查时看到/var/lib/docker/containers/id/*-json.log单个文件 20GB。解决办法是给 Docker daemon 配置日志轮转。编辑/etc/docker/daemon.json{ log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 } }max-size表示单个日志文件达到 100MB 就滚动max-file表示保留最近 3 个文件。改完后重启 Dockersudo systemctl restart docker需要明确这个配置只对新创建的容器生效已有容器需要重新创建才会采用新日志策略。所以最优时机是在搭建 Docker 环境时就配好而不是等磁盘满了再处理。另外容器内应用自己写文件到/var/log或者数据目录不会受这个轮转策略影响该限制的其实只有输出到标准输出的那一路。6. 一次启动失败的完整排查链路从 ExitCode 到端口绑定讲了这么多理论最后用一个完整的实战案例把刚才的命令串起来。场景很简单一个 MySQL 8.0 容器启动后秒退需要定位原因并解决。第一步确认容器真实状态。直接docker ps看不到已退出的容器这是新手最容易懵的地方。必须用docker ps -adocker ps -a | grep mysql假设输出显示Exited (1)说明容器启动后立刻挂了最后退出码是 1。第二步看日志。日志是容器自身的口供docker logs --tail 50 mysql_container_id这个案例里日志里出现[ERROR] [MY-010931] [Server] mysqld: File ./binlog.index not found。这个信息指向数据目录内容损坏或不完整。第三步用 inspect 确认挂载情况。这个报错很可能和之前挂载的空目录有关docker inspect -f {{json .Mounts}} mysql_container_id发现数据目录确实被挂载到了一个宿主机空目录但目录里缺了初始化文件。原因清楚了之前 MySQL 初始化时数据目录权限不对导致初始化没完成容器异常退出。解决方式先停掉容器清空宿主机挂载目录里的残留内容再用正确的权限重建容器docker stop mysql_container_id docker rm mysql_container_id rm -rf /data/mysql/* docker run -d --name mysql \ --restart unless-stopped \ -v /data/mysql:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORDyour_password \ mysql:8.0等待几秒后docker ps | grep mysql docker logs --tail 30 mysql_container_id看到ready for connections后再验证端口映射docker port mysql以及容器内能否监听docker exec mysql ss -tulnp | grep 3306这一套走下来一次启动失败就从一个抽象的状态变成了一组具体的原因链。熟练之后大多数容器启动类问题排查不超过三分钟——第一步docker ps -a看退出码第二步docker logs看报错第三步docker inspect看配置和挂载严格按照这个顺序来你就不会在错误的方向上浪费时间。最后分享一个我自己的操作习惯每次创建容器时都会把docker run命令里的关键参数记下来包括端口、挂载、环境变量、重启策略放在一个 README 或注释里。几个月后容器出问题你还能根据原始参数反推当时的设计意图而不是对着docker inspect的输出猜这个容器当初是怎么建的。Docker 命令不难难的是在使用它的过程中建立一套属于自己的排查逻辑——这套逻辑比任何命令本身都值钱。
返回列表