ARTICLE DETAIL

资讯详情

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

Docker核心三要素:镜像、容器、仓库实战全解析

Docker核心三要素:镜像、容器、仓库实战全解析 折腾过Docker的人基本都绕不开这三个词镜像、容器、仓库。我最初接触Docker的时候看到各种教程里频繁出现“docker pull 镜像”、“docker run 容器”、“docker push 到仓库”说实话是一脸懵的。但后来把这几个概念想通了发现Docker并没有想象中那么玄乎它本质上就是一套“打包-运输-运行”的标准化流程。这篇文章不搞复杂理论就用最直白的语言把这三大核心概念彻底拆开再结合我实际踩过的坑、日常部署中遇到的真实场景让你三分钟内建立起对Docker的完整认知框架。写这篇文章的初衷也很简单我见过太多新手在安装完Docker之后卡在“镜像和容器到底啥区别”“启动容器之后怎么访问”“为什么我commit了个镜像却push不上去”这类问题上。这些问题一旦想明白后面学Docker Compose、K8s都会顺畅很多。所以这篇文章适合刚装好Docker、准备认真用起来的开发者也适合那些用Docker部署过几个服务但一直“知其然不知其所以然”的人。1. 镜像像做菜用的“模具”只读不可改1.1 镜像不是一台虚拟机而是一套只读模板理解镜像最简单的方式是想象你在做月饼模具是固定的不管你用模具做出多少个月饼模具本身不会变。Docker镜像就是这个模具它把所有需要的文件、依赖、配置、代码全部打包成一个只读模板。你用这个模板创建出来的“月饼”就是容器。这个只读特性非常关键。它意味着镜像本身永远不会被运行中的程序改动程序运行产生的新文件、修改的配置都会写入容器自己的可写层。我见过有人把镜像当成虚拟机来用进入容器后改了配置文件然后一重启容器发现配置又变回去了折腾了半天才意识到要重新构建镜像或者使用数据卷。这个坑在初学阶段几乎人人都会踩。严格来说镜像是由一层一层的只读文件系统叠加而成的每一层对应Dockerfile里的一条指令。比如FROM centos:7 RUN yum install -y net-tools COPY app.jar /opt/app.jar CMD [java, -jar, /opt/app.jar]这个Dockerfile会生成三层基础操作系统层、安装工具层、拷贝应用层。每一层都相当于是“在上一层基础上新增了什么”的记录。这样做的好处是多个镜像可以共享底层相同的基础层比如你拉取了十个不同的CentOS镜像底层公共层只会在硬盘里存一份大量节省磁盘空间和网络带宽。1.2 分层机制像Git提交记录一样层层叠加镜像分层这个概念很多人容易忽略但它是理解镜像体积、构建缓存、镜像命名的钥匙。Docker镜像的每一层都是只读的它们像乐高积木一样堆叠在一起。当你修改镜像时实际上不是在原层上修改而是新增一层覆盖上去。这跟Git的提交记录很像每一次提交都是基于上一次状态的增量历史记录永远保留。我在构建镜像时最常用的一个技巧就是利用分层缓存。Docker在构建镜像时如果某一行Dockerfile指令对应的层之前已经构建过且没有变化就会直接使用缓存秒完成。所以合理的Dockerfile写法是把变动频率低的内容放在前面、变动频繁的内容放在后面。比如先COPY requirements.txt再执行依赖安装最后才COPY源码。如果顺序反了只要源码改一行依赖安装那一步就得重新跑一遍白白浪费几分钟。查看分层信息可以直接用docker history 镜像名:标签你会看到每一层的ID、创建时间、占用的存储体积。这在排查“镜像为什么这么大”的时候特别有用。1.3 镜像从哪来三个来源镜像的获取途径无非三种从仓库拉取、用Dockerfile构建、从容器提交。从仓库拉取最常见一条命令搞定docker pull nginx:latest用Dockerfile构建适合自己定制镜像比如打包Java应用、Node应用docker build -t my-app:1.0 .从容器提交这种方式我一般不推荐在生产环境用。它就是把容器的当前状态整个打包成一个镜像docker commit 容器ID my-app-backup:20260601这种做法的弊端在于镜像层会膨胀而且你完全说不清楚这个镜像里到底包含什么不具备可复现性。我只有在临时需要备份容器现场的时候才用commit常规项目一律用Dockerfile。2. 容器镜像跑起来之后的“活体”2.1 容器的本质是一个隔离的进程如果说镜像是模具那容器就是用模具做出来的月饼——它是镜像的可运行实例。但更准确地说法应该是容器是一个被隔离的进程。它运行在宿主机上但通过Linux Namespaces技术拥有自己独立的文件系统、网络、进程号、用户视角。很多新手以为容器里跑的就是一个完整的操作系统其实不是。容器里只有一个进程以及这个进程运行时需要的文件和依赖。你进入容器执行ps命令看到的进程列表非常简单因为容器看不到宿主机的其他进程。这就是容器和虚拟机的核心差异虚拟机跑的是完整客户机操作系统容器跑的是共用宿主机内核的隔离进程。我通常用一句话给团队新人解释虚拟机是你租了一间带全套家具的房子容器是你住在一个大房子里但你的房间是独立的你可以按自己的喜好布置房间但你和大房子里其他人共用水电和承重墙。2.2 容器的生命周期与状态管理容器的生命周期其实很好记创建、启动、停止、销毁。对应命令分别是docker create / docker start / docker stop / docker rm。常用命令# 用nginx镜像创建并后台启动一个容器命名为web1映射80端口 docker run -d --name web1 -p 80:80 nginx:latest # 查看正在运行的容器 docker ps # 查看所有容器包括已停止的 docker ps -a # 进入容器内部 docker exec -it web1 bash # 停止容器 docker stop web1 # 删除容器 docker rm web1这里有个经验要分享docker run是create和start的合并操作但容器启动后进程崩溃退出容器状态会变成Exited。排查问题时第一件事就是docker ps -a因为docker ps默认只显示运行中的容器一个个排查如果没有这个习惯经常会“找不到容器去哪了”。2.3 容器资源隔离与限制容器虽然共享宿主机内核但可以通过Cgroups技术做资源隔离和限制。你可以给容器规定它最多能用多少CPU、多少内存、多少磁盘IO。这在多容器共存的物理机上尤其重要不设限制的容器可能会把宿主机资源吃光直接影响其他服务。常用资源限制参数# 限制内存最大512MCPU最多用1.5个核心 docker run -d --name app --memory 512m --cpus 1.5 my-app:1.0我实际部署过一台服务器同时跑MySQL、Redis、Nginx和几个Java应用的场景如果不做资源限制某个Java应用出现内存泄漏宿主机可能直接OOM崩掉。加了限制之后最坏情况只是这个容器被杀死其他服务不受影响。这也是容器相对裸进程的一大优势。需要说明的是资源隔离不等于安全隔离。容器仍共享宿主机内核如果某个容器获得了root权限并且存在内核漏洞理论上可能穿透隔离边界。所以不要把不可信的东西随便丢进容器里更不要随意给容器加--privileged参数。2.4 容器的可写层与数据持久化容器启动后会获得一个可写层所有运行时产生的文件、日志都写在这里。但这里隐藏着一个大坑容器一旦被删除这个可写层连同里面的数据会一起消失。操作日志、数据库文件、上传的附件全部没了。解决这个问题靠数据卷Volume和挂载目录# 把宿主机的/data/mysql目录挂载到容器的/var/lib/mysql目录 docker run -d --name mysql -v /data/mysql:/var/lib/mysql mysql:8.0挂载数据目录是我每次部署数据库类容器必须做的事没有例外。我的习惯是宿主机上每个服务固定一个目录比如/data/mysql、/data/redis、/data/app这样即使容器被误删重建数据也还在备份也方便。3. 仓库存放和分发镜像的“码头”3.1 仓库到底在解决什么问题镜像打包好了如果只在本地用那直接docker images就能管理。但现实中的需求是团队协作、多机部署、环境迁移这时候就需要一个公共或私有的“码头”来存放镜像让不同机器都能下载使用。这个码头就是仓库。仓库的概念其实用过Maven的人应该秒懂。Maven仓库存放的是jar包Docker仓库存放的是镜像。远程中央仓库比如Maven Central对应Docker Hub私有Nexus仓库对应Docker Registry。我在用Maven配置私服的时候核心诉求是加速依赖下载、统一内部构件版本用Docker私有仓库的诉求几乎一模一样——加速镜像分发、保存自研镜像、绕过公网的不确定性。3.2 镜像命名规则与标签要熟练使用仓库必须理解镜像名的组成。一个完整的Docker镜像名是仓库地址/命名空间/镜像名:标签。比如docker pull registry.cn-hangzhou.aliyuncs.com/acs/mysql:8.0这个镜像名拆开后含义是阿里云杭州仓库registry.cn-hangzhou.aliyuncs.com、命名空间acs、镜像名mysql、标签8.0。如果不带仓库地址默认去Docker Hub拉取。如果不写标签默认取latest。标签是版本管理的核心。我一开始图省事所有镜像都打latest结果后来越来越混乱完全不知道线上跑的是哪个版本的镜像。后来强制自己每个镜像都带版本标签比如my-app:1.0.0、my-app:1.1.0回滚的时候直接切换标签就行非常爽快。3.3 push与pull的完整操作流程推送镜像到仓库的操作流程是固定的# 1. 给镜像打上仓库标签 docker tag my-app:1.0.0 registry.example.com/myteam/my-app:1.0.0 # 2. 登录仓库 docker login registry.example.com # 3. 推送 docker push registry.example.com/myteam/my-app:1.0.0拉取的时候只要仓库可访问就直接docker pull。这里容易踩的坑是本地构建的镜像没有打仓库地址前缀push的时候会得到denied: requested access to the resource is denied。原因在于Docker默认把不带仓库地址的镜像名当成Docker Hub的官方库名。所以push之前一定要先tag把仓库地址补全。如果团队内网络条件有限又想快速分发镜像可以自己搭一个Registry。一条命令就能跑起来docker run -d --name registry -p 5000:5000 registry:2然后本地推送到这个地址就行。这个方案在局域网内分享镜像非常实用配合Nginx做HTTPS或放在内网网段即可。我有一次在机房内网迁移服务几百台机器要部署同一套环境直接在内网搭了个Registry批量docker pull比U盘拷贝效率不知道高了多少倍。3.4 镜像加速与仓库配置经验国内拉取Docker Hub镜像经常速度感人这里不讨论任何绕过网络限制的手段但配置镜像加速器是合规且普遍的做法。常见思路是在Docker配置文件中设置registry-mirrors{ registry-mirrors: [https://docker.1ms.run] }配置完成后重启Docker服务即可生效sudo systemctl restart docker拉取前可以先确认镜像源是否可用我一般用docker info查看Registry Mirrors配置再找一个镜像测试拉取速度。配置之后如果还慢换个镜像源地址再试不同地区不同网络环境下效果差异很大。4. 一条线串起三者镜像、容器、仓库如何协作4.1 完整工作流拉取、运行、修改、推送把三个概念放在一条流水线里整个协作关系就非常清楚了# 第一步从仓库拉取镜像 docker pull nginx:1.24 # 第二步基于镜像创建并运行容器 docker run -d --name web -p 8080:80 nginx:1.24 # 第三步进入容器做一些自定义修改 docker exec -it web bash # 比如修改nginx配置、安装工具等在容器内部操作 # 第四步把修改后的容器提交为新镜像 docker commit web my-nginx:1.0 # 第五步给新镜像打标签并推送到仓库 docker tag my-nginx:1.0 registry.example.com/myteam/nginx:1.0 docker push registry.example.com/myteam/nginx:1.0这个工作流里镜像和容器的关系就像“类和实例”镜像是类定义容器是实例化出来的对象。你可以从一个镜像创建无数个容器每个容器都是独立运行的互不干扰。仓库则是这个体系的“版本控制系统分发网络”保证你拿到的一定是这个版本号对应的那套代码、依赖和配置。4.2 部署Java应用的完整示例纸上谈兵不够我用一个最常见的Java应用部署场景把整个过程走一遍。假设你有一个Spring Boot项目的jar包想把项目容器化部署。第一步写DockerfileFROM openjdk:8-jdk-alpine COPY app.jar /opt/app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /opt/app.jar]第二步构建镜像docker build -t bookstore:1.0 .第三步运行容器并验证docker run -d --name bookstore -p 8080:8080 bookstore:1.0 docker ps curl http://localhost:8080/health第四步把镜像推送到仓库docker tag bookstore:1.0 registry.example.com/apps/bookstore:1.0 docker push registry.example.com/apps/bookstore:1.0第五步在另一台机器上部署docker pull registry.example.com/apps/bookstore:1.0 docker run -d --name bookstore -p 8080:8080 registry.example.com/apps/bookstore:1.0看到没有从开发机到生产机的迁移全程只需要镜像名加上版本号。这就是仓库存在的价值它让“这份代码这份运行环境”变成一个有版本、可分发、可追溯的产物。4.3 镜像安全与容器安全镜像和容器是Docker里最容易出现安全问题的两个位置。镜像安全的核心在源头拉取的镜像是不是可靠的。之前出现过恶意镜像事件有人往镜像里植入挖矿程序或盗取数据的脚本一旦拉到本地运行宿主机会被拖下水。我的习惯是只从官方仓库或信誉良好的私有仓库拉取镜像拉取前用docker image inspect查看镜像的创建时间、Entrypoint、暴露的端口重点检查是否有异常脚本。自建镜像时尽量使用精简基础镜像比如alpine减少攻击面。容器安全的核心在运行时不随便加--privileged、不把容器端口裸奔到公网、定期更新镜像补丁内核漏洞。容器隔离不等于绝对安全这一点必须时刻记在心里。5. 实操过程中最容易踩的坑与排查技巧5.1 Docker Desktop突然启动失败Windows上使用Docker Desktop最常见的报错之一就是“virtualization support not detected”或“Docker Desktop failed to start because virtualization support is not detected”。这个问题的本质是宿主机的虚拟化功能没有开启。排查步骤先确认BIOS里Intel VT-x或AMD-V已经开启再去Windows功能里确认“虚拟机平台”和“适用于Linux的Windows子系统”都被勾选。如果两项都确认了仍然失败用管理员权限打开PowerShell执行bcdedit /set hypervisorlaunchtype auto然后重启电脑。大概率问题就解决了。还有一个小概率情况是电脑上装了其他虚拟机软件比如VMware、VirtualBox和Hyper-V产生了冲突这就需要考虑切换Docker Desktop的后端或者关掉冲突的虚拟化软件。5.2 容器运行MySQL却连不上我用Docker安装MySQL踩过两次比较深的坑一次是容器跑起来了但宿主机连接超时另一次是时区不对。连接超时的典型原因是指定了localhost却发现宿主机端口映射没问题但容器内部MySQL服务没起来。排查思路不用乱猜先看日志docker logs mysql如果日志显示ready for connections说明服务本身没问题接着排查端口映射docker ps --format table {{.Names}}\t{{.Ports}}确认宿主机端口有没有正确映射到容器3306端口。如果映射了还是连不上再排查防火墙和MySQL的用户授权问题。MySQL容器默认只允许root在localhost登录远程连接需要额外创建用户并授权这是非常容易忽略的点。5.3 容器里启动sshd失败有些场景需要在容器内跑sshd方便调试比如直接拿CentOS 7.9容器当跳板机或者测试环境使用。但我在容器里启动sshd时遇到过一次典型的失败sshd: no hostkeys found。这个报错的原因很简单容器镜像默认没有生成SSH host key需要先手动生成ssh-keygen -A service sshd start如果是CentOS容器还需要确认sshd服务的启动方式。容器环境没有systemd的进程一号用systemctl start sshd多半会报错因为容器里根本没有init系统正确做法是直接执行/usr/sbin/sshd -D或者把sshd的启动命令写进容器的入口脚本里。这个坑我记忆犹新当时卡了半个多小时才反应过来容器里的“服务管理”和宿主机不是一回事。5.4 容器“没了”但容器里还有数据要抢救如果误删了容器先别慌被删除的容器如果还在宿主机磁盘上留下写层数据可以使用docker commit抢救。但更常见的场景是容器还在但数据没挂载到宿主机路径比如通过docker exec进容器修改了配置、生成了文件容器没删数据就还在。确认容器ID后docker cp 容器ID:/etc/nginx/nginx.conf ./nginx.conf.bak把关键文件复制出来备份。我每次在容器里做临时修改前都习惯先docker cp备份原文件这个习惯帮我避免过多次“改崩了但不知道原文件长什么样”的尴尬。5.5 想让容器直接用宿主机网络怎么办有几次部署服务时我不想让容器走端口映射那套逻辑而是让容器直接使用宿主机的网络环境。比如宝塔面板里装了某些容器应用希望它们跳过NAT直接用宿主机IP对外提供服务。这个需求不用绕弯子Docker原生就支持host网络模式docker run -d --network host --name web nginx:latesthost模式下容器不分配独立IP直接共享宿主机的网络栈。它的好处是没有了端口映射这一层性能损耗通过localhhost就能直接访问容器内的服务。坏处是端口冲突极其直接容器里监听一个端口宿主机上就不能同时监听同一个端口否则立刻报address already in use。还需要注意host网络模式在Docker Desktop的Mac和Windows版本上表现和Linux不一样那两个平台本质上还是跑在一台虚拟Linux机器里所谓“宿主机”其实是虚拟机。所以host模式最合适的场景是Linux服务器上部署追求极简网络链路的场合Windows/macOS上老老实实用bridge模式加端口映射就好。5.6 容器时间不对日志时间差了8小时容器默认时区是UTC导致运行Java应用时日志时间比北京时间少8个小时排查问题时非常容易造成误解。之前遇到过客户反馈“凌晨2点系统自动任务没执行”我一看日志时间全部显示为前一天18点当时就意识到是时区问题。解决办法是在运行容器时加上时区挂载docker run -d --name app \ -v /etc/localtime:/etc/localtime:ro \ -v /etc/timezone:/etc/timezone:ro \ my-app:1.0或者在Dockerfile里直接设置时区ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime这个事看似不起眼但对依赖定时任务、财务日切、报表统计的业务来说时区错乱带来的后果相当严重。做容器化改造时我上来的第一件事就是把时区问题确认清楚省得后面排查别的故障时被日志时间干扰判断。5.7 容器资源占用过高排查容器把宿主机CPU或内存吃满的情况我遇到过不止一次。排查思路通常是这样的先用docker stats查看每个容器的实时资源占用找出嫌疑对象再用docker top 容器ID查看容器内进程的宿主视角PID然后配合top或htop按CPU排序确认具体进程。docker stats --format table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}定位之后再决定是调优应用、加资源限制、还是直接重启容器。注意docker stats显示的CPU百分比是相对宿主机整体而言的如果一台机器只有2核一个容器跑到100%它基本就把一台物理机吃没了。这时候只有提前设置了--cpus和--memory限制才能避免服务之间互相拖累。最后分享一点我个人的使用心得镜像、容器、仓库这三个概念搞明白之后Docker这条船的舵就算握住了。我自己的习惯是每用到一个新镜像先不看文档直接用docker inspect看它开了哪些端口、挂载了哪些路径、默认执行什么命令用这种方式“反推”镜像设计者的意图。很多镜像的常见坑都能从inspect结果里提前看出端倪。比如官方MySQL镜像自带的环境变量、官方Redis镜像默认无密码、Nginx镜像的默认网站根目录路径这些信息全都在docker inspect的输出里躺着。如果让我给刚入门的读者一条最实际的建议不要急着背命令先拿一个nginx镜像反复做“拉取镜像-启动容器-修改配置-提交镜像-推送到私有仓库”这个闭环跑通一遍之后你对镜像、容器、仓库三个概念的体感会完全不一样。光看文章永远只是知道亲手把这条链路跑通那才是真正的会了。
返回列表