ARTICLE DETAIL

资讯详情

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

Docker镜像详解与常用命令实战:从拉取到运行全掌握

Docker镜像详解与常用命令实战:从拉取到运行全掌握 第一次接触 Docker 的时候我在终端敲下docker images屏幕上有一个空表格上面写着 REPOSITORY、TAG、IMAGE ID、CREATED、SIZE。那时候我对“镜像”这个概念的理解还很模糊它到底是个文件还是个程序为什么别人总能一句话说“把项目打成镜像”后来自己啃文档、踩坑、看日志才慢慢把镜像和命令这层关系理顺。这篇东西不打算把 Docker 官方文档搬过来我只想用自己实际操作过的经验把“什么是 Docker 镜像”这件事讲明白再把最常用的命令按照真实使用场景串起来。无论你是刚入门的新手还是已经在用 Docker 但有些命令总是记混的人应该都能从这里拿走点东西。1. “镜像是啥”没那么玄它不是安装包更像一张可运行的文件系统快照很多教程一上来就讲 Docker 是“轻量级虚拟机”这个说法害了不少人。虚拟机会带一个完整的 Guest OS占用几个 GB 很常见但一个镜像可能只有几十 MB。更关键的区别在于虚拟机快照是“整机状态”而 Docker 镜像不是一台机器它是一堆文件和元数据的组合。1.1 一张图类比镜像是一份“可执行的文件系统菜谱”我通常这样给人解释镜像就像一道菜的完整配方里面写明了需要哪些原料依赖库、用什么锅运行时、加多少水环境变量、什么时候出锅启动命令。你照着配方可以做出很多份一模一样的菜这些“菜”就是容器——容器可以吃运行、可以倒掉删除但菜谱写在那里不会变。当你执行docker run的时候Docker 并不是复制一整个操作系统而是基于镜像里的文件系统内容在它上面加一个“可写层”然后启动进程。这也是为什么容器能秒级启动、占资源少。如果只是要跑一个 Nginx镜像里只需要 Nginx 二进制、基础库、配置文件和日志目录不需要一个完整的 Linux 内核——内核用的是宿主机的。1.2 分层机制为什么一个镜像能省空间又能秒级启动镜像内部不是一团糊糊而是由多层只读层组成的。每一层对应文件系统的一次变更。你在 Dockerfile 里写一条RUN、COPY、ADD就会产生一层新的文件系统层。Docker 使用联合文件系统OverlayFS 等把这些层叠加起来对外呈现成一个完整的文件系统。举个例子假设你有一个基础镜像ubuntu:22.04它有三层系统基础文件。你再安装一个 Python会在上面多加一层再复制你的代码又加一层。如果另一个镜像也是基于同一个ubuntu:22.04这两个镜像可以共享底下的系统层不需要各自存一份物理磁盘占用自然就小了。容器的可写层也很关键。容器启动后所有对文件系统的修改都发生在最上面的可写层底下的镜像层完全不会被改动。这是“写时复制”的设计。这样的好处是即使你在容器里把/etc/nginx/nginx.conf改得面目全非底层镜像还是原样但代价是一旦容器被删除可写层也会跟着消失你在容器里改的文件、生成的临时数据都会没掉。很多人刚用 Docker 时都踩过这个坑——容器一删数据没了然后跑来问“我是谁我在哪我的文件呢”所以容器里不能存重要数据要存就得用挂载卷。1.3 镜像、容器、仓库三者的关系别搞混概念角色生命周期类比镜像静态只读模板不运行可长期保存菜谱容器镜像的运行实例可创建、启动、停止、删除做出来的菜仓库存放镜像的服务器远程共享使用菜谱库这个关系是理解后续所有命令的基础。docker pull是从仓库把镜像拉到本机docker run是用本机镜像创建容器docker commit是把容器的当前状态固化成新镜像docker push是把镜像推到仓库。后面讲命令的时候你会发现所有操作都绕不开这条线。2. 镜像从哪来拉取、导入、构建三条主要路径镜像是容器运行的原料没有镜像什么都跑不起来。获取镜像的常规路径有三条从 Registry 拉取、从文件导入、用 Dockerfile 构建。每条路径对应不同的使用场景值得分开来讲。2.1docker pull最常规的获取方式背后是仓库和标签一条简单的docker pull nginx其实做了好几件事。Docker 会先去配置好的 Registry 查找名为nginx的镜像如果不指定标签默认拉取latest。完整写法是docker pull nginx:latest。在实际操作中你会看到类似这样的输出latest: Pulling from library/nginx a2abf6c4d29d: Pull complete c9a4d9d5f5f0: Already exists ... Digest: sha256:9b5f6b... Status: Downloaded newer image for nginx:latest docker.io/library/nginx:latest注意它按层拉取每层都有一个哈希。如果某一层在本地已经存在就会显示Already exists这就是分层复用带来的加速效果。镜像的命名要严格遵守规范。完整格式是[Registry地址/]镜像仓库名[:标签]。比如nginx:alpine表示官方nginx仓库的alpine标签myregistry.example.com/myteam/myapp:v1.0表示某个私有仓库的带版本镜像。如果你想拉取特定架构的镜像比如在 ARM 机器上拉 amd64可以用--platform参数指定不过跨架构运行需要内核支持普通场景下不推荐。2.2docker save和docker load离线传输镜像的救命稻草内网部署、离线安装、机房迁移这些场景下没法直接docker pull只能通过文件传递镜像。这就用到save和load。# 把本地镜像打包成 tar 文件 docker save -o nginx.tar nginx:latest # 压缩保存体积更小 docker save nginx:latest | gzip nginx.tar.gz # 在目标机器上加载 docker load -i nginx.tarsave和load是一对操作对象是镜像。还有个容易混淆的docker export/docker import它俩操作对象是容器——把容器的整个文件系统打包不带镜像的历史层和元数据。我的建议是能用save/load就别用export/import因为load出来的镜像还能保留标签、Entrypoint、环境变量等信息import出来的东西基本等于一个裸文件系统还得重新设置一堆东西很麻烦。2.3 用 Dockerfile 构建从零到一打造自己的镜像镜像不会凭空出现。如果你要跑自己的代码最规范的方式是写一个 Dockerfile然后docker build。一个最简单的 Python Web 服务示例FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 8000]构建命令docker build -t mywebapi:v1 .这里的.是构建上下文Docker 会把当前目录及以下文件发送给 Docker 守护进程所以要避免把node_modules、.git这类大目录放在上下文里。可以用.dockerignore排除。Dockerfile 里的每条指令都会生成一个层这也是为什么你要尽量把需要频繁变动的文件放在后面前面的层能被缓存复用。我见过不少新手把COPY . .写在最前面之后每次改代码后面所有层全部缓存失效构建一次要十分钟。把会变动的依赖文件和不变动的代码分开构建速度能提升一大截。3. 镜像管理命令看列表、改标签、删除、清理一个都不能少镜像拉下来之后日常打交道最多的就是docker images、docker rmi、docker tag、docker inspect、docker history。它们都不难但有些细节不注意就会踩坑。3.1docker images不要只会看列表docker images会列出本地镜像的仓库名、标签、镜像 ID、创建时间和大小。几个实用变体# 查看所有顶层镜像包括中间层镜像默认只显示最终镜像 docker images -a # 查看悬空镜像dangling image即没有标签、没有关联容器的镜像 docker images --filter danglingtrue # 只看 nginx 相关镜像 docker images nginx输出的IMAGE ID是镜像内容的哈希前 12 位。一个镜像 ID 可能对应多个REPOSITORY:TAG因为它们本质上指向同一个镜像只是别名不同。所以别看到 ID 一样就觉得奇怪这是正常的。真正要留神的是磁盘占用。docker images上显示的SIZE是单个镜像的“可见大小”但因为有共享层实际总占用可能没有把所有镜像的 SIZE 加起来那么大。想精确看占用用docker system df能看到镜像、容器、卷、构建缓存的占用情况。3.2docker rmi删除镜像时为什么老是报错删除镜像用docker rmi或docker image rm。用法很简单docker rmi nginx:latest docker rmi 5d4e8dd8e2d4 # 也可以用镜像ID但如果你有一个容器还在基于这个镜像运行哪怕是已停止的容器没删掉你也删不掉镜像会看到这样的报错Error response from daemon: conflict: unable to remove repository reference nginx:latest (must force) - container abc123 is using its referenced image原因是 Docker 需要保证容器能重新启动。解决顺序是先把使用这个镜像的容器删掉docker rm 容器ID再删镜像。如果确实不想保留容器可以直接docker rm -f强制删除运行中的容器。不建议用docker rmi -f强制删除镜像因为会导致依赖该镜像的容器处于“坏掉”的状态下次启动会找不到镜像层。批量清理悬空镜像我推荐docker image prune -f这个命令会删除所有没有标签且没有被容器引用的镜像层。如果还想顺便删掉所有未使用的镜像用docker image prune -a -f但要注意它会把你本地没有正在运行容器的镜像全部删掉下次要用就得重新拉慎用。3.3docker tag和docker push给镜像“改名字”的门道docker tag其实是给已有镜像添加一个别名不会复制镜像内容。常见用途有两个一是给本地镜像重新打个版本号二是准备推送到私有仓库。# 把本地镜像 myapp:v1 打个 tag 成 myapp:prod docker tag myapp:v1 myapp:prod # 为推送到某个 registry 打 tag docker tag myapp:v1 registry.example.com/myteam/myapp:v1推送镜像前必须先登录目标仓库docker login registry.example.com docker push registry.example.com/myteam/myapp:v1如果你没打 registry 前缀就直接docker push myapp:v1Docker 会尝试推到 Docker Hub 上你的命名空间下。但如果你本地这个镜像的标签还有一层 Hash直接推会失败会提示does not have a tag之类的。这种事我踩过把公司私有镜像打了latest结果不小心推到 Docker Hub虽然使了点劲撤回来但教训很深。3.4docker inspect和docker history扒开镜像的“内脏”看docker inspect输出的是镜像或容器的详细 JSON 配置包括环境变量、工作目录、入口命令、端口声明、层级信息等。平时排查问题最常看这几项docker inspect nginx:latest | jq .[0].Config.Env docker inspect nginx:latest | jq .[0].Config.Cmd docker inspect nginx:latest | jq .[0].Config.Entrypoint不过大多数人更喜欢docker history它能显示镜像的每一层是怎么生成的。比如docker history nginx:latest输出里能看到每一层对应的 Dockerfile 指令、层大小、是否内联缓存。虽然history输出的命令不一定是可读的原生 Dockerfile有些是自动生成的但用来排查“为什么镜像这么大”“哪一层占空间”非常有用。如果你想准确定位每层占用推荐结合docker image history --no-trunc查看完整命令再用docker system df -v看每层大小。4. 容器生命周期命令从镜像到运行实例的关键一步镜像本身不会跑只有创建成容器才能执行进程。容器生命周期相关命令是最常用的也是最容易把新手绕晕的。4.1docker run参数最多但记住几个核心的就行docker run的完整参数一百多个日常用到的不超过十个。一条典型命令docker run -d --name web01 -p 8080:80 -v /data:/usr/share/nginx/html --restartalways nginx:latest含义拆开解释-d后台运行。不加-d的话容器会占着当前终端CtrlC 会停掉容器。--name web01给容器指定一个名字后面操作直接用名字不用记 ID。-p 8080:80把宿主机的 8080 端口映射到容器的 80 端口。格式是宿主机端口:容器端口。注意容器端口是容器内进程监听的端口宿主机端口不能冲突。-v /data:/usr/share/nginx/html把宿主机的/data目录挂载到容器的/usr/share/nginx/html容器内对这个目录的读写会直接落到宿主机容器删了数据还在。--restartalways当容器退出或 Docker 守护进程重启时Docker 会自动拉起这个容器。生产环境常用的还有unless-stopped。如果只是临时跑个交互式容器测一测可以这样docker run -it --rm alpine sh-it就是-i加-t保持标准输入打开并分配一个伪终端用来进容器里敲命令--rm表示容器退出时自动删除。这个组合对测试特别友好退出即清理不用手动删除。4.2ps/start/stop/restart/rm别搞混状态docker ps只看正在运行的容器。docker ps -a看所有容器包括已停止的。docker start启动一个已存在的、处于停止状态的容器。docker stop优雅停止容器会发送 SIGTERM过一段时间再 SIGKILL。docker restart等价于 stop start。docker rm删除容器不能删除运行中的需要先 stop 或加-f。这里有个容易忽略的点docker stop不是立即杀掉进程它先发 SIGTERM等待容器的STOPSIGNAL触发的优雅退出逻辑。默认有 10 秒的宽限期之后才发 SIGKILL。如果容器里是数据库或消息队列你需要确保应用能处理 SIGTERM 并完成收尾。需要调长宽限可以用-t参数docker stop -t 30 容器名。4.3exec与logs排查容器问题的最强组合日志几乎是排查问题的第一入口。想看容器日志docker logs web01 docker logs --tail 200 web01 docker logs -f web01-f会持续输出日志类似tail -f。默认日志来自容器的 stdout/stderr如果你在容器里通过print、console.log、log4j之类的输出到控制台都能看到。如果想进入一个正在运行的容器里去看状态、改配置、跑命令用execdocker exec -it web01 bash但注意很多精简镜像例如alpine没有 bash只有sh。所以更保险的写法是docker exec -it web01 /bin/sh如果连/bin/sh都没有说明这个镜像真的精简到了极点你可以用docker cp拉文件出来看或者用docker exec执行某个二进制。exec和docker attach的区别attach是直接进入容器的“主进程”控制台这会挂到 PID 1 的标准输入输出上如果你在里面按 CtrlC很有可能把容器主进程给停掉。而exec是另起一个新的进程不会干扰主进程。所以日常调试几乎只推荐exec。4.4cp与commit文件互传和“歪门邪道”生成镜像从容器里拿文件出来或者把文件塞进容器用docker cp# 把容器里的日志拷出来 docker cp web01:/var/log/nginx/access.log ./access.log # 把本地文件拷进容器 docker cp ./index.html web01:/usr/share/nginx/html/cp在容器运行中也能用非常方便。但它只是传输文件不会影响镜像。如果要“根据容器当前状态做一个新镜像”可以用docker commitdocker commit web01 myweb:manual-v1我强烈建议只在应急时用commit不要当作常规构建方式。因为 commit 出来的镜像存在几个问题无法从 Dockerfile 复现操作历史无法审计容易把容器里的临时文件、日志、敏感信息一并打进去镜像层会包含你在容器里手动做过的所有变更体积可能膨胀。要让镜像“可管理、可复现”永远优先写 Dockerfile用build构建。5. 完整案例用 Nginx 镜像跑一个可改页面的网站前面全是知识点现在把它们串起来。我们以一个典型场景为例拉取 Nginx 镜像修改默认页面理解镜像层和容器数据。这个过程能覆盖一大半刚才提到的命令。5.1 拉取镜像并启动容器先拉取一个 Nginx 镜像docker pull nginx:alpine然后直接运行docker run -d --name web01 -p 8080:80 nginx:alpine启动后访问http://localhost:8080你会看到 Nginx 的欢迎页。docker ps确认容器已运行CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES abc123def456 nginx:alpine /docker-entrypoint.… 10 seconds ago Up 9 seconds 0.0.0.0:8080-80/tcp web01这步成功说明镜像拉取、容器创建、端口映射都正常了。5.2 修改默认页面容器内是临时的挂载才是持久的尝试修改 Nginx 默认页面。先看文件路径docker exec web01 ls /usr/share/nginx/html/然后写一个新的index.htmldocker exec -it web01 sh cd /usr/share/nginx/html echo h1Hello Docker/h1 index.html exit刷新页面你会看到内容变了。但现在如果你执行docker stop web01 docker rm web01 docker run -d --name web02 -p 8080:80 nginx:alpine再刷新页面内容又变回“Welcome to nginx!”——为什么因为刚才的修改只发生在web01容器的可写层容器删除后那一层也没了。这就是前面反复强调的数据易失性。正确做法是用挂载卷。先把容器删了重新带卷运行docker rm -f web01 mkdir -p /data/nginx echo h1Hello Docker Volume/h1 /data/nginx/index.html docker run -d --name web03 -p 8080:80 -v /data/nginx:/usr/share/nginx/html nginx:alpine再刷新页面内容就是Hello Docker Volume。即使web03被删除重新创建只要挂载目录还是同一个数据就不会丢。5.3 用history观察镜像分层搞清楚 Nginx 镜像为啥这么小nginx:alpine完整大小也就几十 MB和nginx:latest基于 Debian相比小很多。用docker history看看docker history nginx:alpine你能看到一层层 ADD、RUN、CMD 等历史。最底下是 Alpine Linux 的基础层上面是 Nginx 相关层。这就是“同一个基础镜像能被多个镜像复用”的实际呈现。如果想看更多细节可以看镜像的配置docker inspect nginx:alpine | head -n 40你会看到Entrypoint是一个脚本Cmd是nginx -g daemon off;。这也是为什么 Nginx 容器能在前台运行而不是 daemon 化——Docker 容器要求主进程在前台否则容器会立即退出。很多新手自己写 Docker 跑服务发现容器起来就死多半是因为把进程放到了后台。6. 高频命令速查与几个我踩过的坑最后这部分是实用到不能再实用的东西。一份速查表加几个真实经历能帮你省不少时间。6.1 一组高频命令速查表命令作用例docker pull 镜像:标签拉取镜像docker pull mysql:8.0docker images列出本地镜像docker images -adocker rmi 镜像删除镜像docker rmi mysql:8.0docker tag 源 目标给镜像打标签docker tag myapp:v1 myapp:proddocker push 镜像推送镜像到仓库docker push myapp:proddocker save -o 文件.tar 镜像导出镜像docker save -o mysql.tar mysql:8.0docker load -i 文件.tar导入镜像docker load -i mysql.tardocker run -d --name 名 -p 宿主机:容器 镜像创建并后台运行容器docker run -d --name web -p 80:80 nginxdocker ps/docker ps -a查看运行中/所有容器docker ps -adocker start / stop / restart 容器操作容器状态docker restart webdocker rm 容器删除容器docker rm -f webdocker exec -it 容器 sh进入容器docker exec -it web /bin/shdocker logs 容器查看日志docker logs -f webdocker cp 容器:路径 本地路径从容器拷文件docker cp web:/var/log/nginx/access.log .docker inspect 容器/镜像查看详细信息docker inspect webdocker history 镜像查看镜像构建历史docker history nginxdocker image prune清理悬空镜像docker image prune -fdocker system df查看 Docker 磁盘占用docker system df建议你把这表收藏或背下来然后用一个虚拟环境多练几遍。命令不用刻意记多用几次就会了。6.2 配置国内镜像加速源拉镜像速度起飞如果你在本地docker pull时经常卡在 “Layer already being fetched by another client” 或者慢到怀疑人生大概率是网络原因。在配置 Docker 镜像加速器之前可以先试试公共 Registry Mirror。做法编辑/etc/docker/daemon.json没有就新建{ registry-mirrors: [ https://docker.m.daocloud.io, https://dockerhub.icu, https://docker.1panel.live ] }然后重启 Dockersudo systemctl daemon-reload sudo systemctl restart docker不同加速源的稳定性和速度不一样而且有些可能改地址。最稳的是使用云厂商提供的专属加速器地址——阿里云、腾讯云、华为云的容器镜像服务控制台里都能找到一个专属域名按它们的文档配置就好。配好之后再用docker pull你会发现下载速度快了很多。注意这个配置只影响从 Docker Hub 拉取镜像不影响你推镜像或从私有仓库拉取。6.3 命令之外我踩过的几个真实坑容器时区问题。很多基础镜像默认时区是 UTC导致日志时间和本地时间差 8 小时。以前我排查线上问题看到异常日志时间怎么都对不上后来才意识到是容器时区问题。解决方法是运行容器时挂载/etc/localtime或在 Dockerfile 里设置ENV TZAsia/Shanghai。不要在生产环境随便用docker system prune -a。这条命令会删除所有没有正在运行的容器使用的镜像包括你之前为某个旧版本打好的回滚镜像。我干过一次事后要回滚发现镜像全没了只好重新构建非常狼狈。清理前先用docker system df -v看清楚会删什么。镜像标签真的别乱用latest。你拉下来的latest可能和你同事拉下来的latest不一样因为镜像会更新而latest始终指向最新的那个。这在测试环境倒没什么生产环境一定要锁定确切版本号最好用摘要digest锁定。我以前踩过生产环境用的nginx:latest跨版本升级后配置直接不兼容的坑后来统一改成nginx:1.25.3这种精确版本。容器里不要同时跑多个进程。我见过有人在一个容器里既跑 PHP-FPM 又跑 MySQL 还跑 Redis美其名曰“节省资源”。这样做容器会特别难管理日志混在一起重启策略也会很混乱。Docker 的设计理念是一个容器一个主进程通过多个容器编排来解决依赖问题。Kubernetes 出现之前你在 Compose 中拆多个 service 也能把这事办清楚。关于非 root 用户运行 Docker如果你用 Linux 且不是 root命令前通常要加sudo。不想每次加sudo可以把用户加入docker组sudo usermod -aG docker $USER然后重新登录。但要注意加入docker组等同于获得宿主机 root 权限因为可以挂载宿主机目录并获得 root 写权限。所以在生产环境、团队服务器上要不要给这个权限需要慎重考虑。至于 Docker Desktop如果用的是 Windows 或 macOS安装好之后你可以在终端里直接使用同样的命令行命令。它只是把 Docker 引擎包装了一层图形界面底层命令和 Linux 上是一致的。遇到 Docker Desktop 启动失败、提示 virtualization support 或 WSL 相关问题优先检查 BIOS 里的虚拟化开关、Windows 功能里是否启用了 Hyper-V 或 WSL2。这些是环境层面的问题和镜像命令本身关系不大。最后说一个我用了很久的小技巧在 shell 的配置文件里加几个别名能让日常操作快不少alias ddocker alias dpsdocker ps alias dpsadocker ps -a alias dimagesdocker images alias dexecdocker exec -it alias dlogsdocker logs -f alias dstopalldocker stop $(docker ps -q)加上别名之后很多操作从一个长串缩成三个字母。不过别名只是锦上添花核心是你真正理解了“镜像是只读模板容器是临时实例”这一套逻辑。搞懂这个你再去看别人写的一堆命令基本都能猜出七七八八。Docker 的入门曲线不算陡真正难的是把容器当作一种组织软件的思维方式。这篇文章里的东西足够你日常使用和排查问题了剩下的交给遇到问题时再去翻文档。
返回列表