ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Docker CLI实战指南:镜像、容器、网络与Compose编排全解析

Docker CLI实战指南:镜像、容器、网络与Compose编排全解析 我这些年接触过的工具里Docker的命令行是最值钱也最容易被忽视的一块。很多人装完Docker Desktop习惯了点点鼠标拉镜像、点启动、看日志等到真要上服务器部署、做自动化、排查生产环境问题时一下就懵了——因为服务器上根本没有图形界面所有操作都得靠命令行。这篇文章不是让你背命令而是把Docker的CLI从底层逻辑到实战用法串一遍覆盖镜像管理、容器生命周期、网络、数据卷、Compose编排、日志排障这些核心场景。我会用自己的真实操作经验来讲每一步为什么这么做、踩过什么坑适合刚装好Docker想系统学命令的人也适合已经用过一阵但还在靠记忆碎片拼命令的人。1. 先从整体看Docker命令行1.1 Docker CLI是什么为什么我坚持用命令行而不是图形界面Docker命令行工具就是那个叫docker的程序。装好Docker之后你在终端敲docker version能跟Docker引擎对话的就是它。整个CLI的架构其实很简单所有命令都长成一个样子——docker接一个对象类型比如image、container、network、volume再接一个动作比如pull、run、ls后面跟参数。理解了这个套路你根本不需要背命令看到一条没见过的命令也能猜个大概。为什么我坚持用命令行最直接的原因是服务器上只有命令行。生产环境部署、CI/CD流水线里跑构建、凌晨被警报叫起来看容器状态这些场景清一色都是SSH进终端敲命令。Docker Desktop的图形界面只适合本机学习时点点看看它和CLI能力并不完全对等。很多细节调整、资源限制、网络配置图形界面要么藏得很深要么根本没有入口。而且命令行是可脚本化的一次写好的操作可以复制到任何机器执行这是图形界面永远做不到的。另外一个原因是命令行能帮你建立正确的认知模型。你在终端敲docker run的时候你是在明确地告诉引擎我要用哪个镜像、开放哪个端口、挂载哪个目录、设置什么环境变量。这一条命令写下来容器的构成就清清楚楚。而点界面操作你只是在一个表单里填空填完就忘了出了问题根本不知道从哪查起。1.2 命令体系快速地图我给新手整理Docker命令时习惯先把命令按对象分成四大类这样记忆负担会小很多。第一类是镜像相关核心动词是pull拉取、images列出本地镜像、rmi删除镜像、tag打标签、build构建镜像、push推送镜像到仓库。镜像可以理解成容器的“安装包”它只读不能直接运行运行起来才有进程和状态。第二类是容器相关核心动词是run创建并启动、ps列出容器、start/stop/restart启停、exec进入容器执行命令、logs查看日志、rm删除容器、cp宿主机和容器间复制文件。容器是镜像的运行实例相当于“安装包解压后在跑的程序”。第三类是资源类包括network网络、volume数据卷、image、container这些子命令组。网络解决容器之间怎么通信、端口怎么暴露的问题数据卷解决容器删了数据丢不丢的问题。第四类是系统维护类比如info查看引擎信息、version查看版本、system df查看磁盘占用、system prune清理无用资源、stats实时查看容器资源占用。这张地图你只要在脑子里建立了后面就是往每个格子填充细节的事情。接下来我就按这条主线把高频命令一个个拆开讲。2. 高频核心命令逐条拆解2.1 镜像操作pull、images、rmi、tag、build先讲拉取镜像。命令是docker pull nginx:1.25这里nginx是镜像名1.25是标签。标签很容易被忽略实际上它是镜像世界里最需要养成习惯的一个参数。默认不写标签会拉latest但latest的含义是“最近更新”不是“最稳定”生产环境镜像一定要锁定明确的版本号。我自己的习惯是所有跑业务的镜像都写具体版本比如mysql:8.0.36绝不写latest。好处是半年后再看这机器你知道它跑的是哪个版本的软件坏处是升级要手动改但这类操作本来就不该自动。拉完之后用docker images查看本地镜像列表。输出里有仓库名、标签、镜像ID、创建时间和大小。注意镜像ID和容器ID都只显示前12位但实际是64位的你用docker命令操作时写前几位能唯一匹配就行。比如docker rmi 3f8也能删掉一个ID以3f8开头的镜像。这个技巧在紧急清理时要快很多。删除镜像用docker rmi删除前你要确认没有正在运行的容器依赖它。如果删不掉报错提示“image is being used by stopped container”那就得先删容器再删镜像或者用docker rmi -f强制删。我一般不推荐-f因为你强制删掉之后那个容器的运行记录还在以后想再用这个镜像就得重拉而且数据依赖如果挂载了外部卷还好没挂的话会很麻烦。打好标签是镜像管理中很有用的操作。docker tag myapp:v1 registry.example.com/myapp:v1这条命令不改镜像内容只是给同一个镜像多了一个名字和标签。它在你准备推送到私有仓库、或者需要给镜像保留一个版本别名时非常关键。很多人不理解tag和build的区别build是重新生成镜像层tag只是给已有镜像加别名瞬间完成。最后是构建镜像docker build -t myapp:1.0 .。那个点不能漏它代表构建上下文目录。这里有个非常常见的坑构建时会把你指定的目录整个发给Docker守护进程如果目录里有node_modules、target这种几百MB甚至上GB的东西构建会卡到怀疑人生。所以项目里一定要写.dockerignore文件和.gitignore一样随手维护把不需要进镜像的目录全列进去。我之前见过一个同事把整个home目录当构建上下文构建了半小时没结束最后发现他在发整个用户目录给守护进程这种操作改成项目目录加.dockerignore之后构建时间直接缩短到几十秒。2.2 容器操作run、ps、exec、logs、stop、rmdocker run是使用频率最高的命令它实际是“创建容器启动容器”的合并操作。一个最典型的运行命令长这样docker run -d \ --name mysql-8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDMyPass123 \ -v mysql-data:/var/lib/mysql \ -v /etc/mysql/conf.d:/etc/mysql/conf.d:ro \ mysql:8.0.36逐个解释一下。-d表示后台运行detach不加你就得一直挂着终端CtrlC容器就会停。--name给容器起名字这非常重要后续的docker exec、docker logs、docker stop都靠这个名称定位容器。-p 3306:3306是端口映射左边是宿主机端口右边是容器内端口意思是访问宿主机3306就相当于访问容器的3306。-e设置环境变量MySQL的root密码就是这么传进去的。-v是数据卷挂载这里有两种写法。mysql-data:/var/lib/mysql是命名卷由Docker管理存储位置在主机上你不知道它具体在哪但数据是持久化的推荐给数据库这种需要无脑备份的数据用。/etc/mysql/conf.d:/ect/mysql/conf.d:ro是绑定挂载把宿主机目录直接映射进容器冒号后面加ro表示容器内只读适合配置文件这种你不想让容器随便改的东西。两者的核心逻辑都是“容器活数据放在宿主机上容器删了数据还在”。容器启动后用docker ps看运行中的容器列表加-a可以看到已经停止的容器。输出里的状态列有几种可能Up表示运行中Exited表示已退出Restarting表示重启中。如果看到一个容器一直Restarting八成是启动命令崩溃或健康检查没通过要去查日志。查日志用docker logs 容器名。加-f可以像tail -f一样持续追踪输出加--tail 100只看最后100行。这是排查问题的第一站。docker logs默认只显示标准输出和标准错误如果程序写日志到文件而不是stdout你会看不到内容所以设计容器内的应用时让程序把日志打到stdout是最省心的方案。要进一个正在运行的容器内部执行命令用docker exec -it 容器名 bash。-it是-i加-t的组合-i保持标准输入打开-t分配一个伪终端没有它你进不去交互式shell。出了容器用exit退出。有人说“进容器看看吧”其实很多场景根本不用进去比如看进程列表直接docker exec 容器名 ps aux看环境变量直接docker exec 容器名 env一条命令就能搞定少一次交互就少一分风险。停止和删除也有讲究。docker stop是优雅停止给进程发送SIGTERM信号让程序自己收拾行李docker kill是直接SIGKILL强制断电。优先用stop。删除容器用docker rm但docker rm -f可以强制删除运行中的容器相当于先kill再rm。日常清理时我经常用docker rm -f但你要清楚它其实是不太优雅的数据库类的容器千万别这么干否则可能丢数据。2.3 网络与数据卷network、volumeDocker网络这块很多人一知半解但部署多容器应用时绕不开。默认情况下每个容器有自己的网络命名空间互相之间用IP通信但IP会变所以你得让容器用名字互相访问。最简单的方式是把几个容器放到同一个自定义网络里。docker network create my-net创建一个自定义网络然后启动容器时加上--network my-net同一个网络内的容器就可以直接用容器名互相访问。比如你跑了一个MySQL容器叫mysql-8再跑一个应用容器加了--network my-net在应用容器里连接数据库时主机名直接写mysql-8就行不用查IP。用默认的bridge网络做不到这一点这也是我从来不推荐新手用默认--link参数的原因--link是老掉牙的方案它在容器里加hosts条目模拟名字解析扩展性很差自定义网络才是现在该用的做法。docker network ls列出所有网络默认有三个bridge、host、none。bridge是默认的NAT网络端口映射靠它实现host是让容器直接共享宿主机的网络栈性能最好但缺少隔离none是关闭网络。自定义网络默认也是bridge模式所以默认端口映射规则同样适用。排查网络问题时docker network inspect my-net能查看该网络下所有容器的IP和网关。我曾经遇到两个容器同一个网络里ping不通最后发现是防火墙规则拦了容器间通信inspect一下看到IP和网段后到宿主机防火墙里去放行对应网段才解决。数据卷这边docker volume create volume名手动创建卷docker volume ls列出卷docker volume inspect看卷挂载在哪里。不过我实际使用中更常用的是直接-v在run命令里隐式创建。需要单独备份卷数据时可以用一个挂载了卷的临时容器配合tar打包比如docker run --rm -v my-volume:/data -v $(pwd):/backup alpine tar czf /backup/my-volume.tar.gz -C /data .这条命令启动一个临时alpine容器把目标卷挂到/data把当前目录挂到/backup然后打包卷内容到宿主机。任务结束--rm会自动删除这个临时容器干净利落。2.4 系统级操作info、version、system先看docker version和docker info。前者显示客户端和服务端的版本信息后者显示引擎的详细状态包括存储驱动、CPU/内存限制、镜像和容器数量、数据根目录位置等。排查问题时docker info是第一步比如你发现容器一直创建失败先看引擎是否正常、磁盘是否满了、存储驱动是不是需要调整。docker system df输出一个很直观的资源占用表告诉你镜像、容器、本地卷、构建缓存分别占了多少空间。这个命令是做磁盘清理前必看的。紧接着docker system prune会清理所有停止的容器、无用的网络、悬空的镜像和构建缓存加-a连未使用的镜像也一并清掉。我一直提醒团队日常开发机上定期跑一次docker system prune -a --volumes要慎重因为--volumes会连没有容器引用的数据卷一起删数据卷里往往是珍贵的数据库数据全没了找不回来。我不建议轻易加--volumes宁可先docker volume ls看看有没有确定的临时卷。还有个很有用的实时监控命令docker stats能看所有运行中容器的CPU、内存、网络、磁盘IO占用。排查容器资源占用过高时开个独立终端跑docker stats一眼就能锁定是哪个容器在吃内存。3. 实战一个标准项目的完整生命周期3.1 选型与准备这一节我用一个真实的部署流程串起来场景是在一台干净的Ubuntu服务器上用Docker部署一个MySQL 8.0数据库加一个Nginx反向代理最后再通过Docker Compose把这套组合写成一个可复制的工作流。开始之前先用docker version确认CLI能正常连上引擎。如果报错先看Docker服务有没有启动Linux上通常是systemctl status docker。然后把基础镜像拉下来建议走官方仓库比如docker pull mysql:8.0.36和docker pull nginx:1.25。为什么用具体版本而不用latest前面说过了我再强调一次生产环境永远用锁定版本避免某一天apt update之后环境突然变了根因查半天结果是镜像标签漂移了。3.2 拉镜像、启动容器MySQL示例我用前面那串docker run命令启动MySQL。这里有几个细节值得展开。第一个是数据目录的选择。我给数据库推荐命名卷而不是绑定挂载因为命名卷的备份方式统一不用关心具体路径。如果一定要绑定挂载要提前检查宿主机目录权限MySQL容器里的mysql用户UID是999宿主机目录如果属主不对容器内无法写入启动直接失败。我踩过的坑就是直接把/data/mysql挂在宿主机上但忘了改权限容器总是秒退日志里一堆Permission denied最后chown -R 999:999 /data/mysql解决。第二个是时区问题。很多镜像默认时区是UTC业务查日志时对不上本地时间。我通常在run命令里加-e TZAsia/Shanghai这个环境变量对大多数官方镜像都有效。有的镜像不认这个变量那就得在容器内自己改但99%的场景加这个参数就够了。第三个是初始化SQL脚本。第一次启动MySQL容器时它会自动执行/docker-entrypoint-initdb.d目录下的.sh、.sql文件。我把初始化库表的脚本挂进去docker run -d \ --name mysql-8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDMyPass123 \ -e TZAsia/Shanghai \ -v mysql-data:/var/lib/mysql \ -v /data/mysql/init:/docker-entrypoint-initdb.d:ro \ mysql:8.0.36这个目录只在首次初始化时执行如果你容器已经跑过了改里面的脚本不会重新执行要手动进容器执行SQL。这个机制弄清楚部署数据库就心里有底了。启动完用docker logs mysql-8 --tail 50看初次启动日志看到ready for connections就说明起来了。再用docker exec -it mysql-8 mysql -uroot -p进MySQL验证连接。这一套流程下来数据库就绪前后不到一分钟手动装MySQL光配置就要折腾半天这就是Docker的价值所在。3.3 多容器协同Compose单容器用docker run还能应付容器多了之后逐条写命令不仅繁琐还容易漏参数。这时候用Docker Compose。Compose的配置文件是YAML下面是我常用的一套模板services: db: image: mysql:8.0.36 container_name: mysql-8 ports: - 3306:3306 environment: MYSQL_ROOT_PASSWORD: MyPass123 TZ: Asia/Shanghai volumes: - mysql-data:/var/lib/mysql - /data/mysql/init:/docker-entrypoint-initdb.d:ro restart: unless-stopped healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 10s timeout: 3s retries: 5 web: image: nginx:1.25 container_name: nginx-web ports: - 80:80 volumes: - /data/www:/usr/share/nginx/html:ro depends_on: db: condition: service_healthy restart: unless-stopped volumes: mysql-data:几个关键点说一下。restart: unless-stopped表示除非手动停掉否则容器退出后会自动重启服务器重启也会跟着起来这比裸-d跑容器稳妥太多。healthcheck是MySQL容器的健康检查这里用mysqladmin ping检测数据库是否真的可连接。depends_on配合condition: service_healthy保证Nginx容器只在数据库健康后才启动避免出现应用先起但连不上数据库的经典时序问题。在项目目录执行docker compose up -dCompose会读取当前目录的compose.yaml或docker-compose.yml按声明顺序启动服务。之后改代码、改配置执行docker compose down清理掉整套再docker compose up -d重新拉起。这样一套环境的启停、重建都固化在配置里换一台机器复制目录过来就能一键复现这是我平时交付环境的标准姿势。3.4 发布与清理开发机上镜像调好了要发布到服务器最简单的方式是推到镜像仓库后到服务器上拉。没有私有仓库就用Docker Hub把镜像打上自己账号的标签推送服务器上docker pull再docker run。如果机器之间网络不方便也可以docker save导出tar包再docker load导入但这个方法只适合没有仓库的临时场景规范做法永远是仓库。清理这一步容易被忽略。容器部署一段时间后镜像和构建缓存会吃掉大量磁盘。我一般每个月或在大版本更新时执行一次docker system df docker system prunesystem prune会删除所有已停止的容器、悬空镜像、未使用的网络和构建缓存。加不加-a看你自己我通常不加因为-a会把所有未被容器引用的镜像也删掉下次回滚旧版本就要重新拉代价更大。只清理悬空镜像一般能释放大部分空间。4. 常见问题和排查技巧4.1 连接Docker引擎失败在Windows桌面版上最常见的报错是类似“failed to connect to the docker api at npipe:////./pipe/dockerdesktop-linux”或者“virtualization support not detected”。这类问题的根因基本都在Docker Desktop没起来、WSL2或Hyper-V后端异常。我的排查顺序是先看任务栏的Docker图标状态再看系统设置里的虚拟化支持是否开启最后用docker version验证是否恢复连接。在Linux服务器上连接失败通常是Docker服务没启动或者当前用户不在docker组。启动服务用systemctl start docker要开机自启就systemctl enable docker。非root用户直接执行docker命令报permission denied的话那是因为docker的守护进程socket文件/var/run/docker.sock默认只能root访问把用户加入docker组sudo usermod -aG docker $USER newgrp docker把用户加进docker组确实能解决问题但要注意docker组和root权限基本等价容器可以挂载宿主机的任何目录所以这个操作只该在信任的机器上做公司生产服务器上不要随意给开发加docker组。4.2 网络不通容器无法访问宿主机以外的网络或者容器之间ping不通现象五花八门原因通常是三类。第一类是宿主机防火墙拦截。容器通过NAT访问外部iptables规则没放行对应网段出不去也进不来。检查方法是用docker network inspect bridge看容器的网段比如172.17.0.0/16再看防火墙有没有放行这个网段。第二类是端口映射没生效。docker ps里看端口列如果3306/tcp显示成了0.0.0.0:3306-3306/tcp说明映射正常。如果是127.0.0.1:3306-3306/tcp就表示只允许本机访问外部机器连不上。这个是有意配置时用的部署时要注意你选了哪种绑定地址。第三类是DNS问题。容器内解析不了域名时可以看看宿主机/etc/docker/daemon.json里的dns配置或者启动容器时用--dns 8.8.8.8指定DNS。国内服务器尤其容易碰到容器内curl不通、但宿主机通畅的情况多半就是DNS没配上。4.3 权限错误最常见的权限错误就是前面提到的容器内数据目录无法写入。启动容器后日志里出现Permission denied第一反应是看那个挂载目录在宿主机上的属主和权限ls -l /data/mysql。如果目录属主是root而容器内进程以mysql用户运行就会拒绝写入。解决办法是调整宿主机目录属主或者用--user参数改变容器内运行的用户但我更建议前者因为改变容器用户可能引发其他兼容问题。还有一类是执行docker命令时持续的权限错误比如Cannot connect to the Docker daemon。这类问题在Linux上多半是环境变量DOCKER_HOST指错了地方。我看过有人配了DOCKER_HOSTtcp://127.0.0.1:2375然后什么命令都连不上。排查时先echo $DOCKER_HOST看有没有异常有就unset DOCKER_HOST再说其他原因。4.4 磁盘空间占用大docker system df一看镜像几GB、构建缓存几GB这是常态。构建缓存是docker build留下的中间产物占用尤其夸张。解决方案是定期docker builder prune清理构建缓存不影响已有镜像。如果连镜像也想清理用docker image prune -a删除所有未被使用的镜像执行之前先确认没有要回滚的版本。还有一个冷门但实用的点Docker日志文件默认没有大小限制长年累月会撑爆磁盘。在/etc/docker/daemon.json里配置{ log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 } }然后systemctl restart docker。这样每个容器的日志文件最大10MB、滚动保留3个文件磁盘占用就稳住了。这个配置要尽早设置因为已有的容器日志不会自动重新切分你得重建容器才能生效。5. 我的日常最佳实践5.1 命名规范与标签命名看起来是小事实际影响很大。容器名我统一用“服务名-环境-序号”的格式比如mysql-prod-01、nginx-dev-01。镜像标签用“版本号-构建时间”的格式比如myapp:1.0.2-20250115。这样在几十个容器里一眼能看出这是哪个服务、哪个环境、哪个版本排查和生产回滚时效率差别非常大。很多团队出事之后才补规范不如一开始就定好。给镜像打标签时强烈建议同时推两个标签一个是精确版本号一个是latest或stable之类的滚动标签。程序部署引用精确版本人工快速测试引用滚动标签两不耽误。但这只适合业务镜像依赖的官方基础镜像还是锁版本别用latest。5.2 别名和脚本高频命令可以配置shell别名能省不少事。我的~/.bashrc里加了几行alias ddocker alias dcdocker compose alias dldocker logs -f --tail 100 alias dpsdocker ps --format table {{.Names}}\t{{.Status}}\t{{.Ports}} alias dexecdocker exec -itdocker ps的--format很有用你可以自定义输出列只看关心的字段。这样一条dps出来每个容器的名字、状态、端口映射整整齐齐比默认输出清晰太多。脚本化方面我给自己的原则是凡是要执行超过三次的操作就写成脚本。比如一条完整的部署流程从构建、推送、服务器拉取、重启到清理旧容器用Shell脚本串起来或者直接写一条Makefile目标。有了脚本部署变成可重复的过程也不怕某一步手抖了。5.3 学习路径建议如果你刚接触Docker命令行我建议按以下顺序练习先把pull、images、run、ps、logs、exec、stop、rm这八个命令练熟用Nginx或Redis练手反复启停、改端口、挂数据卷。然后学习network和volume把两个容器放到自定义网络里互相访问再理解数据卷为什么能保存数据。接着学build和Dockerfile把自己写的脚本构建成镜像。最后学Compose把之前手动跑的多容器流程固化成配置文件。这套路径不求快但每一步都建议在终端里真实跑一遍。命令行这个东西看一百遍教程不如亲手敲一遍。你真敲过一遍忘了还能靠--help找回来完全没敲过看命令就跟看天书一样。我自己到现在每隔一阵还会用docker help扫一眼子命令列表看看有没有被我忽略的功能。Docker迭代很快新命令会不断出现保持探索的心态比试图一次性记全所有命令更实际。把核心逻辑掌握住剩下的都是查文档的事。
返回列表