ARTICLE DETAIL

资讯详情

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

Docker镜像与容器核心命令实战:从拉取到持久化的完整操作指南

Docker镜像与容器核心命令实战:从拉取到持久化的完整操作指南 上一篇把 Docker 装好之后很多人会卡在同一个地方知道docker run能跑容器可什么时候该加-it什么时候用-d镜像跟容器到底谁是谁其实我带过不少新人发现最能拉开差距的不是网络架构而是对镜像和容器这两层关系的理解以及命令使用习惯的细节。这一篇是系列第二篇专门把镜像与容器的核心操作命令逐个拆开讲从拉镜像到删容器从端口映射到数据持久化最后附上两个可以直接照抄的实战场景。不管你是刚装好 Docker Desktop 的 Windows 用户还是服务器上跑 docker-ce 的运维只要把这篇里的命令吃透日常开发和部署基本就够用了。1. 先把镜像、容器、仓库这三层关系捋清楚1.1 镜像到底是什么只读模板加分层结构很多人把镜像理解成安装包这个说法容易误导。安装包装上就完事了镜像不是装一次而是每次运行都基于它重新创建容器。我更愿意把镜像比作预装好系统的光盘光盘只读、内容固定、可以反复使用用它开机一次就得到一台新电脑关机丢掉也完全不影响光盘本身。从技术层面看镜像由一层一层的只读文件系统叠加而成每一层对应 Dockerfile 里的一条指令。比如基于哪个基础系统是一层、安装 nginx是一层、拷贝配置文件是一层。启动容器时Docker 会在这些只读层之上再叠加一个可写层这个可写层就是容器运行时的状态。你执行docker history nginx:latest能看到每一层的大小和操作这就是理解镜像底层的直观方式。这个设计带来的实际好处有两个一是镜像可以复用和增量传输同一个基础镜像被多个镜像共用时本地只需存一份二是容器之间天然隔离两个从同一镜像启动的容器互不干扰改文件只改自己的可写层。1.2 容器是镜像的运行态实例不是虚拟机容器和虚拟机最大的区别是共享内核。虚拟机里跑着独立的操作系统内核容器只是宿主机上的一个普通进程通过 Namespace 做隔离、通过 Cgroups 做资源限制。也就是说你在宿主机上执行top能看到容器里的进程也在跑只是它的视角被关在了一个独立空间里。这个概念直接决定了一批命令的行为。镜像自己不会跑只有docker run 镜像名才创建容器。而且容器跟主进程绑定主进程退出容器就停止。docker run ubuntu:22.04跑完立刻退出因为 ubuntu 镜像默认没有常驻进程docker run -d nginx能一直后台运行因为 nginx 是前台守护进程。初学的时候最需要纠正的思维是先有容器再往里面装东西。正确姿势是在镜像里定义好一切run 的时候直接得到现容器。需要临时进容器调试就 exec不要动不动进入一个容器改装环境因为容器删了这些改动全会丢。1.3 仓库、镜像名与标签的完整语义一条docker pull nginx:1.24-alpine看起来简单实际上拆开是仓库地址 命名空间 镜像名 标签。默认仓库是 Docker Hub官方镜像的完整写法是docker.io/library/nginx:1.24-alpine其中library是官方镜像的命名空间个人上传的镜像一般是docker.io/你的用户名/镜像名:标签。平时拉取可以不写仓库地址但推送到自己的私有仓库时必须写全。比如推到自建 registrydocker tag myapp:1.0 registry.example.com/myapp:1.0再docker push registry.example.com/myapp:1.0。标签也不止是版本号更是一种方便的引用方式——latest最常用但也最危险生产环境尽量锁定具体版本否则某天基础镜像更新了你重新拉取后容器行为可能完全变样。2. 镜像操作命令拉取、查看、删除、导入导出一套带走2.1 拉取镜像之前先解决源的问题docker pull看着简单但国内网络环境下拉取 Docker Hub 镜像经常慢到怀疑人生。解决思路是配置 registry mirror也就是镜像加速器。Linux 上编辑/etc/docker/daemon.json加入{ registry-mirrors: [https://你的加速器地址] }保存后执行systemctl restart docker生效。Docker Desktop 用户不用敲命令在 Settings - Docker Engine 里找到同一个配置项改完 Apply Restart 即可。这里提醒一句加速器地址建议用你自己云厂商或者可信服务商提供的不要随便填来路不明的地址。配置好之后可以docker info查看 Registry Mirrors 一栏确认是否生效。拉取镜像时还有个习惯值得培养优先选带后缀的精简版比如nginx:1.24-alpine、redis:7-alpine。Alpine 系列比普通版小一半以上默认不装 bash 等多余组件对大部分线上场景完全够用还能显著降低镜像拉取时间和磁盘占用。2.2 docker images 查看与 docker rmi 删除docker images是使用频率最高的镜像查看命令它列出本地所有顶层镜像。加-a会把中间层镜像也列出来这些是构建过程中产生的缓存层一般不需要管。实战中我更常用两个技巧过滤和自定义格式。docker images --filter danglingtrue docker images --format table {{.Repository}}\t{{.Tag}}\t{{.Size}}dangling指没有 tag 的悬空镜像构建新版本后老镜像经常变成这样。清理它们用docker image prune只删悬空镜像很安全想清理所有不用的镜像可以加-a但操作前一定确认没有容器在引用。删除镜像用docker rmi 镜像名或ID。如果镜像被某个容器使用即使容器已经停止直接删除也会报错提示冲突。要么先删容器docker rm 容器名要么用docker rmi -f强制删但-f会留下容器引用的孤儿配置不推荐在生产环境用。2.3 镜像的导入导出save/load 与 export/import 别搞混离线环境迁移镜像是运维常见需求这时用docker save和docker load。docker save -o nginx.tar nginx:1.24把镜像保存成 tar 包docker load -i nginx.tar在目标机器恢复。save 保存的是完整镜像包含所有历史分层、环境变量、入口点信息迁过去后的镜像和原来一模一样。有人会问docker export和docker import不也能导入导出吗确实能但千万别混用。export 导出的是容器的文件系统不是镜像它把容器当前的可写层和只读层合并后扁平化成一份记录不保留镜像的元信息。用 export 导出的包再 import 回来CMD、ENTRYPOINT、ENV 全没了跑起来行为完全不一样。跨环境迁移、镜像分发只认 save/load 这一对。3. 容器生命周期命令从 run 到 rm 全流程拆解3.1 docker run 参数拆解一条命令看清全貌docker run是 Docker 最重要的命令一条典型启动命令长这样docker run -d \ --name my-nginx \ -p 8080:80 \ -v /data/nginx:/usr/share/nginx/html \ -e TZAsia/Shanghai \ --restartalways \ nginx:1.24-alpine逐个看参数-d后台运行不加的话终端会被日志刷屏CtrlC 容器就停了--name给容器命名之后所有操作都可以用名字代替 ID-p做端口映射宿主机 8080 映射到容器 80-v挂载数据卷宿主机/data/nginx映射到容器内网站目录-e设置环境变量这里把时区设成上海--restartalways定义重启策略Docker 服务重启或者容器异常退出时会自动拉起。另一个高频用法是交互式容器docker run -it --rm ubuntu:22.04 bash。-it等于-i加-t保留标准输入并分配终端相当于进入一台新开的 Linux--rm表示退出即自动删除容器适合临时调试和实验。这里有个最常见的认知坑docker run每次执行都会创建一个新容器不是复用已有的。重复跑两次同样的命令会产生两个不同名的容器如果没指定--name端口映射会冲突。已经创建的容器想重启用docker start想进入用docker exec想换参数只能删了重建。3.2 docker exec 进容器做事的正确姿势容器起来了怎么进里面执行命令注意docker exec是在已经运行的容器里执行新命令不是进入容器。最常用的写法docker exec -it my-nginx bash如果容器本身就比较精简比如 alpine 镜像里面只有sh没有 bash就换成docker exec -it my-nginx sh。还有场景是容器里没有 shell那就直接执行具体命令比如docker exec my-nginx nginx -t检查配置语法docker exec mysql8 mysql -uroot -p直接进数据库客户端。docker exec有几个我实际用得很频繁的补充参数-u root指定以 root 身份执行容器默认用户可能不是 root-w /app指定工作目录-e VARvalue临时加环境变量。举个例子某个服务容器默认是非 root 用户你想在容器里改配置文件直接docker exec -it -u root 容器名 sh就能获得 root shell。有人会问docker attach呢attach 是把当前终端接到容器的主进程上命令执行完或者主进程退出终端就断了后台容器还会停掉体验很糟糕。生产环境我基本只用 execattach 更多用于调试没有 ssh 的临时容器。3.3 容器的启停删与自动恢复策略容器创建之后的管理核心命令是 start、stop、restart、rm。docker stop是优雅停止它会先给容器主进程发 SIGTERM 信号宽限期过后再发 SIGKILL。docker kill直接发 SIGKILL 强杀适合主进程卡住不响应的场景。docker restart相当于 stop 加 start有些配置需要重启才生效比如 nginx 加载新证书。docker start启动一个已停止的容器注意它不会改变你之前 run 时定的参数想改端口映射就只能删除重建。删除容器docker rm 容器名删除前必须先停掉容器想一条命令停并删用docker rm -f。清理全量停止容器可以用docker container prune它会提示是否确认避免误删。重启策略值得单独说下。--restart支持的值no默认不自动重启on-failure[:max-retries]只在非正常退出时重启最多重试指定次数always只要容器退出就重启包括 Docker 重启后也会拉起unless-stopped平时和 always 一样但手动 stop 之后Docker 重启时不会强行再启动。生产环境 Web 服务我一般用always或unless-stopped批处理任务则用on-failure避免失败任务无限循环。4. 容器资源限制与权限别让容器拖垮宿主机4.1 内存、CPU 限制的参数与验证方法不设限制的容器能使用宿主机全部资源这在部署多个应用时是灾难。我见过同事的容器因为内存泄漏把整台服务器 OOM 掉所以从第一天起就养成加限制的习惯docker run -d --name app \ -m 512m \ --memory-swap 512m \ --cpus 1.5 \ nginx:1.24-alpine-m 512m限制内存 512 兆。--memory-swap我习惯和内存设成一样的值也就是禁止使用 swap防止容器实际内存超限后靠 swap 硬撑导致性能断崖。--cpus 1.5限制最多使用 1.5 个 CPU 核。如果容器已经创建好了也可以用docker update动态调整docker update --cpus 0.5 --memory 256m 容器名注意有些参数调整后需要重启容器才会完全生效。验证是否生效用docker stats。这个命令实时显示所有运行容器的 CPU、内存、网络和磁盘 IO有点像 Linux 的top但只针对容器。看到某容器内存一直顶在限制线上就该考虑是不是有内存泄漏而不是甩锅给 Docker。4.2 用户、数据卷读写权限与特权模式权限问题是新手重灾区。最常见的是明明挂载了目录容器内进程却写不进去报Permission denied。原因通常是宿主机目录属主是 1000 或者其他 uid容器内进程以别的用户身份在跑。解决思路有三个方向。第一个方向用--user指定容器进程的用户 ID比如--user 1000:1000这样容器进程的 uid 和宿主机目录属主一致自然能读写。第二个方向调宿主机目录权限chown -R 1000:1000 /data/app这在挂载目录给容器用之前先做好。第三个方向是在容器内以 root 先改权限docker exec -it -u root 容器名 chown -R 1000:1000 /app。挂载时还能定义只读权限-v /data/app:/app:ro容器内对 /app 只有读权限适合配置文件、密钥证书这类容器只许读不许改的场景。--privileged是另一个容易出问题的地方。加了它容器几乎等于获得宿主机 root 的全部能力还能访问宿主机设备安全性很差。绝大多数场景用不上它只是容器里需要特定系统能力比如挂载 FUSE 文件系统、操作内核模块那就用更精确的--cap-addSYS_ADMIN这种方式按需添加。我的原则是默认不加特权报缺什么能力再按需补什么能力。另外在 CentOS 这类开启了 SELinux 的环境里挂载目录读写经常莫名报权限错误可以在挂载参数末尾加:z或:Z分别表示共享标签和私有标签这是很多教程不会提但实际很管用的细节。5. 数据持久化与数据卷容器删了数据不能丢5.1 -v 挂载的三种写法各有适用场景容器本身就是短命的删掉后里面的数据跟着消失所以持久化必须用数据卷或挂载目录。-v的写法有三种用 nginx 举例# 方式一宿主机目录挂载 docker run -d --name web -v /home/me/nginx:/usr/share/nginx/html nginx # 方式二具名卷挂载 docker volume create nginx-data docker run -d --name web -v nginx-data:/usr/share/nginx/html nginx # 方式三匿名卷 docker run -d --name web -v /usr/share/nginx/html nginx方式一方便直接改宿主机上的文件开发和测试最常用编辑完刷新页面就看到效果。方式二由 Docker 管理卷的存储位置路径在/var/lib/docker/volumes/nginx-data/_data好处是不用关心宿主机的目录结构删容器后卷还在重建容器直接复用生产环境数据库我强烈推荐用这种方式。方式三是 Docker 自动生成一个随机名卷一般配合 Dockerfile 里的 VOLUME 指令使用平时不太直接用到。判断挂载是否成功用docker inspect 容器名看 Mounts 字段里面有 Source 和 Destination 的对应关系。5.2 数据卷的共享、备份与迁移多个容器要共用一份数据可以用--volumes-from先启动一个挂载卷的容器 A再docker run --volumes-from A 镜像名启动容器 BB 就会继承 A 的卷。虽然现在官方更推荐直接在 run 时指定同一个卷但--volumes-from在容器编排工具还不普及的时期是标准做法遇到老项目时至少要知道它什么意思。备份卷数据我常用的方式是利用一个临时容器docker run --rm \ -v nginx-data:/data \ -v /home/me/backup:/backup \ ubuntu:22.04 \ tar czf /backup/nginx-data.tar.gz -C /data .这条命令直接把卷目录打包到宿主机/home/me/backup下。恢复就是把 tar 包解压回卷的挂载点或者干脆解压到宿主机的某个目录再用方式一挂载进去。对于 MySQL、PostgreSQL 这类数据库别直接 tar 文件目录用数据库自带的备份工具mysqldump、pg_dump更可靠否则很容易备份出损坏的数据文件。6. 网络模式与端口映射怎么访问容器、容器之间怎么通信6.1 bridge、host、none 三种模式怎么选Docker 默认网络模式是 bridge容器会拥有一个独立的虚拟网卡和 IP通常类似 172.17.0.x。bridge 模式下容器通过 NAT 访问外网外面要访问容器里服务要靠端口映射。如果你用的是带 Docker 管理功能的服务器面板比如宝塔、1Panel面板里创建容器时默认就是 bridge容器内部看到的是 172.x 网段的 IP跟宿主机不是一回事。host 模式完全不同容器直接使用宿主机的网络栈没有独立 IP端口直接绑定在宿主机上。这时候-p和-P参数是无效的因为根本不需要映射容器监听 80宿主机的 80 就被占用了。它的好处是网络性能损耗少、延迟低适合对性能敏感的中间件但容器之间的隔离性就差了很多。想让某个容器使用宿主机的网络环境比如要访问宿主机上的特殊网段或者本机回环地址直接--network host是最简单的路径。none 模式就是完全隔离容器只有 loopback 接口适合跑安全敏感或不需要网络的批处理任务。生产环境最常见的组合面向外部访问的服务用 bridge 端口映射跟宿主机网络深度绑定的工具或内网服务用 host。6.2 端口映射细节与随机端口端口映射分两种指定映射和随机映射。-p 8080:80指定宿主机 8080 对应容器 80-P表示把所有镜像里 EXPOSE 过的端口随机映射到宿主机的 32768 之后的端口上测试用可以生产不推荐因为端口不确定。查看当前映射用docker port 容器名输出类似80/tcp - 0.0.0.0:8080。注意宿主机端口的 IP 部分0.0.0.0表示监听所有网卡外网也能访问写成127.0.0.1:8080:80就只允许本机访问适合只想本机调试的场景。想改端口映射没有改的命令必须删除容器、用新端口重新 run所以一开始就要想清楚。6.3 容器之间互联自定义网络才是正确姿势两个容器之间要互相访问最蠢的方式是查 IP 后直连因为容器重建后 IP 会变。正确姿势是创建自定义网络让容器通过名称互访docker network create my-net docker run -d --name app --network my-net nginx:alpine docker run -d --name db --network my-net -e MYSQL_ROOT_PASSWORDxxx mysql:8.0在同一个自定义 bridge 网络里的容器内置 DNS 会自动把容器名解析成对应 IP。应用容器里连接数据库直接写db:3306就行不用关心 db 容器实际 IP 是多少。这个特性是自定义网络自带的默认的 bridge 网络不支持这也是我强烈建议凡是容器间通信一律建自定义网络的原因。顺便说下--link这个老参数它也曾用来容器互联但官方早就标记为过时新项目不建议用统一用自定义网络。7. 两个必练实战MySQL 8.0 和 Redis 主从7.1 用 Docker 跑 MySQL 8.0顺带解决常见的坑第一步拉镜像docker pull mysql:8.0。启动命令建议写清所有关键参数docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD你的密码 \ -e TZAsia/Shanghai \ -v mysql-data:/var/lib/mysql \ mysql:8.0这里用了具名卷mysql-data数据库文件不放在宿主机随意目录保证删容器重建数据还在。-e MYSQL_ROOT_PASSWORD是首次初始化时设置 root 密码的环境变量只对空数据目录生效如果卷里已经有数据这个变量会被忽略。初始化需要一点时间用docker logs -f mysql8看到ready for connections就代表起来了。进入数据库docker exec -it mysql8 mysql -uroot -p然后正常执行 SQL。实际踩坑就多了。端口被本机已有的 MySQL 占用报错提示 bind 失败改成本地端口 3307 就行-p 3307:3306。MySQL 8.0 内存占用偏高512M 内存的机器启动直接 OOMdocker stats看到容器反复退出时要么给机器加内存要么启动参数里追加--innodb-buffer-pool-size64M之类限制。老客户端连 MySQL 8 报认证插件不支持的错是因为默认认证插件caching_sha2_password太新优先升级客户端某些旧版本兼容场景也可以在启动时指定--authentication-policymysql_native_password这类参数改成老的认证方式。7.2 Redis 主从容器搭建Redis 主从我通常用自定义网络比--link干净得多docker network create redis-net docker run -d \ --name redis-master \ --network redis-net \ -p 6379:6379 \ redis:7-alpine docker run -d \ --name redis-slave \ --network redis-net \ -p 6380:6379 \ redis:7-alpine \ redis-server --replicaof redis-master 6379从节点命令里的--replicaof redis-master 6379用容器名作为主节点地址这就是自定义网络 DNS 解析能力发挥作用的地方。老版本 Redis 里这个参数叫--slaveof7.x 开始推荐用--replicaof效果一样。如果不用命令参数也可以挂载自定义 redis.conf 文件在文件里写replicaof redis-master 6379然后启动时指定redis-server /usr/local/etc/redis/redis.conf。验证主从状态docker exec -it redis-slave redis-cli info replication输出里role:slave、master_link_status:up就说明主从关系建立成功。也可以从宿主机连从节点验证redis-cli -p 6380 info replication看到master_link_status:up同样成立。如果状态不是 up多半是两台容器不在同一网络或者从节点配置的主节点地址不对。8. 常见问题与排查技巧实录8.1 容器启动后秒退怎么办容器秒退是最常见的现象尤其新手用docker run -d 某镜像后docker ps什么都看不到。先别慌按这个顺序排查docker ps -a # 看所有容器包括退出的 docker logs 容器名 # 看启动时的输出 docker inspect -f {{.State.ExitCode}} 容器名ExitCode 为 0 说明主进程正常退出比如跑的镜像没有任何常驻任务ExitCode 非 0 说明程序报错日志里通常有具体原因。如果日志信息太少可以用docker run -it --rm --entrypoint sh 镜像名绕开默认入口直接进容器手工执行命令判断是缺环境变量还是命令本身写错了。还有一种情况是容器里主进程 fork 出子进程后自己退出了比如有人误用nginx -g daemon on;容器认为主进程结束就自动停了改成前台运行即可。8.2 端口映射不生效、容器内网络不通宿主机上怎么都访问不了容器的端口先从三个方向查容器本身有没有起来docker ps、映射有没有生效docker port 容器名、宿主机防火墙有没有放行ss -lntp看监听。如果监听地址是127.0.0.1外网当然访问不了重建时改成-p 0.0.0.0:8080:80。用 host 网络模式的容器docker port显示为空因为根本没有映射直接看ss -lntp里宿主机端口。容器内ping不通外网或者apt install报域名解析失败是 DNS 问题。bridge 网络默认把 DNS 指向 Docker 内置的 127.0.0.11某些网络环境下它会失效。在/etc/docker/daemon.json里固定宿主机 DNS{ dns: [114.114.114.114, 8.8.8.8] }重启 Docker 后再重建容器试试。容器之间不通先确认它们是否在同一自定义网络里默认 bridge 网络不能靠容器名互相访问这我已经强调过但还是最常被问到。8.3 日志占满磁盘、容器文件暴涨日志文件无限增长是服务器磁盘被占满的元凶之一。开发环境无所谓生产环境务必做两层防护。第一层在容器启动时限制--log-opt max-size10m --log-opt max-file3表示单个日志文件最大 10 兆、最多保留 3 个。第二层全局兜底在/etc/docker/daemon.json里写{ log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 } }已经堆积的日志文件直接清空truncate -s 0 $(docker inspect -f {{.LogPath}} 容器名)。磁盘占用整体分析用docker system df能看到镜像、容器、卷分别占了多少docker system prune -a清理所有未被使用的镜像和停止的容器执行前确认有-a因为不加-a只会清理悬空镜像。8.4 时区、编码这类日常小坑容器默认时区是 UTC比国内慢八小时。日志时间对不上排查问题非常难受。启动时加-e TZAsia/Shanghai就能解决大部分镜像的时区问题某些非常精简的 Alpine 镜像没有安装 tzdata单纯设置 TZ 变量不生效需要在 Dockerfile 或容器里执行apk add tzdata。还有一些老镜像只认挂载文件可以加-v /etc/localtime:/etc/localtime:ro但更推荐优先用 TZ 环境变量避免依赖宿主机文件系统。另外一个容易踩的是容器内 apt/yum 源在国内环境下安装包特别慢更别提有些精简镜像连 wget、curl 都没有。这类问题不是 Docker 的问题是镜像和网络环境的组合问题建议直接用国内 Linux 软件源替换镜像内默认源或者选自带常用工具的基础镜像。从命令到习惯给新人的一点私货我见过太多人收藏了一堆 Docker 命令真到用的时候还是乱。建议把这篇里的命令拆成三组练镜像组pull、images、rmi、tag、save/load、容器组run、ps、exec、stop、rm、logs、资源组stats、inspect、network、volume每组每天花十几分钟敲一遍坚持一周基本就形成肌肉记忆了。命令本身不值钱值钱的是使用习惯启动容器前先想好名字、挂载卷、端口和重启策略改配置永远选择删除重建而不是进容器乱改生产容器一律加资源限制容器间通信走自定义网络而不是 IP。这些习惯不是哪篇文档写的是踩过坑之后才总结出来的。下一篇我会继续把 Dockerfile 构建和镜像瘦身展开讲到时见。
返回列表