🐎 给前端的 Docker 课程。
前期准备
购买云服务器及连接
好接下来的一段时间呢,咱学习 Docker,推荐呢先准备一台 Linux 服务器,咱这呢给大家演示如何从阿里云来购买这样一个服务器。首先呢需要大家先注册一个阿里云账号,接下来呢,找到顶部的这个产品按钮,鼠标滑上来,找到左侧的精选产品,右边有个云服务器 ECS这个链接,接下来点一下,点完之后呢,就跳转到云服务器 ECS 这样一个购买界面。那接下来,大家可以点一下立即购买这个按钮,点完之后呢,就跳转到这个选择相关配置项的界面,大家可以根据需要进行一个选择,咱这呢给大家逐一进行演示。
首先第一项是关于这个付费类型,大家可以选择按量付费,选择它的好处是,一旦这个服务器实例咱不再需要了可以随时给它释放掉,这样的话它就不会再继续收费了。不过下面也说了有一个注意点,他说啊,按量付费实例不支持备案服务,所以说啊,如果说你需要这个服务的话,那你还是要选择第一个包年包月,OK,这是关于付费类型这一块。
接下来第二项呢,是关于地域的选择,第一个默认的保持不变,第二个框呢,我就选择第一个华北 1 青岛吧。一般情况下呢,你选择的这个地域啊,离你现在所处的这个位置越近,这个服务器实际的访问速度呀,理论上说就会越快一些,所以大家可以根据实际需要进行选择。
ok,再往下是网络及可用区,这里默认就好了。
继续往下,选择实例和镜像的配置,首先是实例这一项,点一下这个全部规格,架构呢可以选择默认的 x86,下面的规格呢,这里列出了好多,大家选择的时候一般来说可以参考这个 CPU 和内存,咱这儿呢仅仅是为了演示 Docker 的学习,所以这些中的任意一个都足够用了。还有一个来说大家可以参考这里的价钱,那我呢就太穷了,所以就选这个经济型最便宜的给大家演示吧。
ok,再往下是关于镜像的选择,咱可以找到公共镜像,点击展开其他镜像,选择乌班图,版本呢,就用第一个最新的,24.04 这个版本。
好了接下来再往下,扩展程序,可以选择你需要的应用或开发环境,他会帮你自动安装,例如咱后面要学习的这个 Docker,但是这儿呢,咱可以先不用选,后面我想带大家学习如何手动安装。
ok,再往下是存储相关的,例如系统盘、数据盘这些,咱不用动,保持默认就好。
接下来再往下呢,是关于这个带宽和安全组这一块,咱需要进行一些操作。首先公网 IP,这个分配公网 IPv4 地址进行打勾,因为后续呢,咱需要根据这个公网这个 IP 地址进行连接或访问,打勾之后呢,下面出来了带宽计费模式,可以选择按固定带宽,带宽值呢,我这儿就选个 1 吧,当然这儿你选的越大肯定也就越贵。
好,接下来还有这个安全组需要选择,咱可以点一下这个新建安全组,点完之后呢,下面有一些可以选择开放的 ipv4 的端口,大家先保持默认的就好。等待会购买完服务器之后,为了方便演示,我会配置所有的端口都开放,当然出于安全性的考虑你也可以进行按需开放。ok,再往下这些都不用管。
接下来划到管理设置这儿,选择自定义密码,登录名就是 root,下面填写你的密码,记住这个填写的密码,后面呢咱需要根据这个密码来登录服务器。
OK 这个写完之后呢,就到最下面了,现在出来了前面选择的这些配置概要,大家可以检查一下看有没有问题,没问题的话呢,可以点击最右下角确认下单,需要大家注意的是,下单的时候要保证你的阿里云账户最少要有 100 块钱。
ok,接下来就出现这样一个购买成功的弹框,点击管理控制台,点完之后呢,就跳转到这样一个界面,那这个就是咱购买的这个服务器实例,接下来咱们点一下这个安全组,找到刚刚咱创建的这个安全组 20250924,点一下管理规则,在入方向,点击手动添加,协议类型选择全部,目的-1/-1,授权对象输入 0.0.0.0/0,点一下保存,这就表示咱们开放了所有端口访问,再次强调,仅仅是为了测试演示方便,生产环境最好不要这样。
ok,接下来我们再点一下实例,找到刚刚购买的这个服务器实例点进去,现在你看到的就是对应的这个实例的详情界面,待会咱需要一个公网 IP 进行后续的连接,所以大家可以找到下面配置信息里面的公网 IP,点击复制,复制完之后呢,大家就可以选择相关的工具进行连接了。
那我这儿用的连接工具呢是 XShell,ok 点一下就启动了 XShell 这个软件,点一下左上角这个新建,输入名称,咱可以写一个比如说 Docker 学习,那这个主机呢,就是刚刚复制的公共 IP,ok 接下来点一下这个连接,点击点击之后呢,会让咱进行选择,例如说接受此主机密钥吗,大家可以选择接受并保存,接下来输入用户名就是 root,可以勾选记住这个用户名,点击确定,接下来输入密码,这个密码就是前面购买服务器时设置的那个登录密码,ok,点击记住密码,点击确定。好,如果你能在这个窗口看到,Welcome 说明咱就连接到这样一个服务器了。
好了,这是本节课咱给大家说的,如何购买阿里云服务器以及该如何进行连接。
安装 Docker
连接到这个服务器之后呢,接下来咱给大家演示一下,如何在这台服务器上面,去安装这个 Docker,那如何安装呢,这时候呢,大家可以打开 Docker 官网,找到 Developers,点击 Documentation,点一下顶部的 Manuals 手册,大家可以找到左侧的 Docker 引擎,展开,接下来点一下这个 install 展开,就出现了相关系统上安装 docker 的一些链接,咱需要在乌班图系统上安装,所以点一下 Ubuntu,点完之后呢,就出现了这个安装相关的一些说明。
接下来点击右侧的 Install Methods,根据步骤进行安装就好啦,首先看一下第一小步,他说要设置 Docker 的 apt 仓库,ok,把这一堆要粘过去,咱右上角点一下这个复制按钮,再次打开 XShell,接下来右键粘贴,接下来敲回车,大家只需要耐心等待就好了,好接下来呢,咱再次打开这个 Chrome,来执行这个第二小步,安装 Docker 相关的一些包,同样把这个命令给它复制一下,再次打开这个 XShell,接下来右键给它进行一个粘贴,敲回车,这时候呢,按一下这个 y 啊,然后接下来继续等他进行一个安装,其实正常来说呀,就是这一步走完之后啊,咱的刀客啊就会安装成功了。
可以通过 docker version 命令来查看安装的版本,可以看到我的版本是 25.0.5。
正常安装 Docker 之后会自动启动,可以通过这个命令来验证一下,如果出现现在这个界面就表示是 ok 的,如果没有启动你可以执行下面这个命令来手动启动一下。
ok,给大家测试一下 Docker 的使用,执行 docker run hello world,敲回车,这个时候他会根据 hello-world 这个镜像来启动一个容器,那如果说本地没有 hello-world 这个镜像的话呢,他会去远端进行一个拉取,至于这个镜像和容器的这个概念啊,咱在后面的时候再给大家进行一个说明,这儿咱先进行测试就好了。ok,哎你会发现呢,此时出现了报错,那这个报错的意思是说啊,他从远端这个地址拉取的时候呢出现了失败,为什么会这样呢,是因为啊,这个地址,他对应的服务器啊在国外,所以说咱就没有拉取成功,那该如何去处理呢?
其实处理的方式会有很多,咱这儿先给大家介绍使用国内镜像代理网站的方式来处理,具体如何去操作呢?
大家可以打开这个网站,点一下使用教程。这里列出了 Linux、MacOS、Windows 等系统的操作方法。咱这儿使用第一个,首先 …最后还要执行下面的命令重启 Docker 服务。重启完毕之后呢,接下来咱再次执行这个 docker run hello-world,回车,首先本地还是没有发现这样一个 hello-world image 镜像,它又去远端进行一个拉取哎,下面列出了它拉取的镜像相关的一些信息,再往下的一些内容就是容器启动后的一些信息输出,ok,看到这些就证明咱后面可以正常的使用 Docker 啦。
其实输出信息之后啊,此时根据 hello-world 这个镜像启动的容器也就停止了,怎么证明它停止了呢,可以通过 docker container ls 或 docker ps 命令查看运行中的容器,发现并没有容器信息输出,这就说明它停止了。这时候可以通过 docker ps -a 查看所有容器(包括停止的),然后这就发现了已经停止的这个容器。需要大家注意的是,并不是说所有容器启动完毕后就会自动停止,例如有些容器提供的是一个服务,这时候这个容器就会一直是运行的状态,当然也肯定有相应命令来去停止。好了,这是关于本节课咱跟大家介绍的如何进行 Docker 的安装和测试,谢谢大家。
相关概念
好接下来咱给大家说一下 Docker 当中比较重要的几个概念,分别是镜像、容器和仓库。
首先看一下镜像,官方说呢,镜像是一个只读模板,包含创建 Docker 容器的说明,而容器呢,是根据镜像运行起来的实例。
咱给大家做一个类比,镜像有点像应用程序安装包,而容器呢,是根据这个安装包安装的运行起来的的应用程序,也就意味着一个镜像可以生成任意多个容器,不同的容器之间是相互隔离的(这里的隔离指的是,各个容器的进程、网络、文件系统等相互独立,互不影响),当然有需要的话也可以通过某些方式进行容期间通信。
最后一个就是仓库的概念,往上翻,咱找到这个 Docker 的架构图,仓库可以分为本地仓库和远程仓库,是用来存储 Docker 镜像的地方,我们可以把本地仓库的镜像上传到远程,也可以把远程仓库的镜像拉取到本地。
通过这个 Docker 架构图也有助于我们理解 Docker 的原理,以及镜像、容器和仓库的之间的关系是怎样的。好咱给大家分析一下这个图,首先他说,Docker 是一个 CS 架构的程序,这个图就呈现了客户端、Docker 主机、远程仓库,这三个核心组件的交互流程。
例如当我们在客户端输入docker run命令的时候,它会向 Docker 主机上的 Docker daemon 或者说叫 Docker 守护进程发送 “运行容器” 的指令。Docker 守护进程检查本地是否存在要运行容器所基于的镜像。如果本地有对应的镜像,会直接使用该镜像启动一个新的容器。
当我们输入docker build命令的时候,它会向 Docker 守护进程发送 “构建镜像” 的指令。Docker 守护进程根据指定的 Dockerfile 进行处理,构建完成后,新的镜像就被存储在了 Docker 主机的本地镜像仓库中,同样的,后续可以根据这个镜像生成容器。
当我们输入docker pull命令的时候,它会向 Docker 守护进程发送 “从仓库拉取镜像” 的指令。Docker 守护进程与远程的 Docker 仓库(如 Docker Hub 等)建立连接,确定要拉取的镜像名称及版本。仓库将对应的镜像数据(包括各镜像层)传输到 Docker 主机的本地仓库。同样的,后续可以根据拉取到的这个镜像生成容器。
ok,结合讲解的这个过程,咱再回过头来给大家分析一下前面运行的这个 docker run hello-world 命令的完整含义:
当我们运行 docker run hello-world 命令的时候,他发现本地没有 hello-world:latest 版本的这个镜像,这也说明了当我们在镜像后面没有加版本的时候,它默认查找的版本是 latest,接下来他会从远端仓库 library/hello-world 拉取 latest 标签或版本的镜像,这个 library 是 Docker 官方镜像的默认命名空间,这儿呢还有一个 sha256 的镜像标识,用来确保镜像的完整性和唯一性,接下来镜像就下载到了本地。
再往下看到 Hello from Docker 这个输出,这是 hello-world 镜像中预定义的输出内容,所以根据此镜像启动容器后就进行了输出。
下面也再次给出了 Docker 的工作步骤:Docker 客户端将命令发送给 Docker 守护进程,Docker 守护进程从远程仓库拉取镜像,并根据镜像启动一个新的容器,最后在终端输出 Hello from Docker 这个内容。
好拉,这是关于本节课咱给大家介绍的 Docker 中的基本概念和相关运行过程,谢谢大家。
关于镜像
拉取
1 | # 默认下载的是最新版本的镜像 |
好,接下来咱学习关于镜像的常见操作,镜像都有哪些常见的操作呢,这个时候咱可以在终端输入 docker image –help 命令来进行查看,那这儿呢就列出了和镜像相关的所有命令,例如 docker image build 构建镜像,history 查看镜像历史,import 导入镜像,inspect 查看镜像的更详细信息等等。
如果某个命令咱不会使用话,同样咱可以通过 docker image 后面跟上一个具体的命令,然后再 –help 来查看这个具体命令的详细用法。
例如我们输入 docker image pull 可以用来拉取镜像,具体如何使用呢,这时候我们可以执行 docker image pull –help,他会告诉我们该如何去使用这个命令,会发现他说 docker image pull 后面可以跟上相关的参数选项,例如 -a、-q,后面再跟上要拉取的镜像的名字,当然也可以通过冒号来指定要拉取的特定标签版本的镜像。另外 docker image pull 这个命令呢,他还有相对应的别名,例如 docker pull 和它是完全等价的。
好接下来我们执行这个命令,给大家进行演示,例如 docker pull nginx:alpine 来下载一个 alpine 版本的 nginx 镜像,这个 alpine 一般用来表示更加轻量级的镜像版本。
查看
1 | # 查看本地镜像 |
如何查看镜像是否拉取成功呢,这时候我们继续通过 docker image –help 来获得帮助,会看到有一个 ls 的命令,他说可以列出所有本地的镜像,此时咱就执行 docker image ls 来查看一下,发现果然存在了这个 alpine 版本的 nginx 镜像。
另外 docker image ls 有没有其他一些等价或者更加便捷的写法呢,这时候我们可以执行 docker image ls –help 来获得帮助,你会发现 docker image ls 其实是 docker image list 的简写,或者也可以通过 docker images 来获得完全等价的效果,其实这种简洁的写法是我们平时用的最多的。
删除
1 | # 删除镜像 |
ok,接下来我们继续看一下如何删除镜像,同样执行 docker image –help,你会发现 rm 这个命令,它可以删除一个或多个镜像,例如咱们输入 docker image rm nginx:alpine,如果你还要删除更多的话,后面以空格隔开再输入其他镜像就好了,敲回车,这时候它就把 nginx:alpine 镜像删除了,再次通过 docker image ls 来查看一下,果然没了。
例如咱再给大家删除一个 hello-world 镜像试一下,执行 docker image rm hello-world,敲回车,此时会发现出现了报错,它不允许我们删除这个镜像,这是因为现在系统当中有根据此镜像产生的容器没有被删除。这时候有两种办法去处理这个问题,第一种办法是先删除根据此镜像生成的容器(这种方法我们后面学到容器的时候再说),第二种办法是强制删除,上面也给出了提示,must force。
那如何强制删除呢,此时可以通过执行 docker image rm –help 来寻求帮助,会发现它后面还可以跟一个 -f 参数表示强制删除,另外我们还发现删除镜像还有一些其他的写法,例如 docker image rm 是 docker image remove 的简写,也可也执行 docker rmi 来获得完全等价的效果。
所以我们可以执行 docker rmi hello-world -f 敲回车,来进行强制删除这个镜像,通过 docker image ls 来查看一下,发现 hello-world 镜像就被删除啦。
好啦,这是本节课咱给大家介绍的关于镜像拉取、查看、删除等操作,谢谢大家。
构建
1 | 121.40.238.160 |
第一种方式
1 | # docker pull nginx:alpine |
当官方或第三方镜像不能满足我们要求的时候,就需要我们自己来构建镜像,构建镜像常见的有两种方式,第一种方式是使用 docker 镜像下的 build 命令,它会根据 Dockerfile 文件中的信息来进行构建,第二种方式是使用 docker 容器下的 commit 命令,它会根据现在的容器来构建镜像。咱这儿呢,先给大家说第一种通过 docker 镜像下的 build 命令来构建镜像。
我们的需求是这样的:构建一个 nginx:v1.0 镜像,根据这个镜像启动容器后,访问容器的 Nginx 服务展示 Hello 文案。
首先我们可以在当前目录新建一个 Dockerfile 文件,例如执行 vim Dockerfile,然后按一下 i,插入一些构建镜像相关的要求,例如 FROM nginx:alpine,这表示拉取 nginx:alpine 这个基础镜像,之后我们可以在他基础上增加或修改一些内容。
往下我们再输入 RUN echo “Hello” > /usr/share/nginx/html/index.html,RUN 指令用于指定在构建镜像过程中要执行的命令,这条命令的作用是向 Nginx 服务默认的网页文件 index.html 中写入 Hello,这会覆盖基础镜像中原有的 index.html 文件,所以当后续我们根据新构建出来的镜像启动容器的时候,访问这个 Nginx 服务,就会看到这个 Hello 字样啦。ok,就写这么多,按一下 esc,然后输入 :wq,保存并退出。
然后执行 docker build -t nginx:v1.0 .,-t 后面跟的是新构建出来的镜像名,这又通过冒号指定了标签版本信息 v1.0,最后一个 . 表示会从当前目录查找 Dockerfile 文件进行构建,那如果你的文件名不叫 Dockerfile 的话,需要通过 -f 参数来指定具体的文件名。
敲回车…ok,接下来通过 docker images 查看本地镜像,发现就新构建出来了 nginx:v1.0 这个镜像,接下来我们通过 docker run 命令来启动容器,例如 docker run -d -p 8089:80 –name ifer-nginx-v1 nginx:v1.0
解释一下这个命令,docker run 表示运行一个容器,完成的写法是 docker container run,他是容器下面的命令,-d 表示后台运行,-p 后面跟的是端口映射,这表示将容器内的 Nginx 服务的默认 80 端口映射到宿主机的 8089 端口,也就意味着我们待会访问的时候需要指定的是 8089,–name 后面跟的是启动的容器的名字 ifer-nginx-v1,如果你不写的话他会随机一个名字,最后这个 nginx:v1.0 是镜像的名字,当然也可以是镜像的 ID,表示要根据这个镜像来启动容器,ok,敲回车,这个时候打开浏览器访问 8086 就能看到这样一个服务内容啦。
ok,敲回车,容器启动成功,接下来我们打开浏览器,访问这个机器的 ip 121.40.238.160,后面跟上 8089 端口,接下来敲回车,如果看到 Hello 字样就表示根据新构建的镜像启动容器成功啦。
ok,敲,看到 Hello,没有问题,好啦,这是本节课咱给大家说的如何使用 docker build 命令来构建镜像,更加高级的使用我们后面还会再说,谢谢大家。
第二种方式
1 | docker exec -it 容器ID bash |
接下来咱继续给大家演示一下构建镜像的第二种方式,即通过 docker 容器下的 commit 命令来根据容器构建镜像。
我们的需求是这样的:构建一个 nginx:v1.1 的镜像,根据这个镜像启动容器后,访问容器内的 Nginx 服务展示 Hello World 文案。
ok,既然需要根据容器来构建镜像,所以我们需要先进入某个容器进行相关操作,把这个容器修改为符合我们要求的样子。
通过 docker ps 命令查看有哪些运行的容器,例如我们希望进入刚刚启动的这个 ifer-nginx-v1 这个容器,可以执行 docker exec -it,后面跟上要进入的容器的 ID 或名字 ifer-nginx-v1,后面再跟上 sh 命令,表示进入容器后会执行这个命令(这儿呢一般也可以使用 bash 命令,不过我们使用的是 nginx alpine 精简版本的镜像启动的容器,默认没有携带 bash 命令),之后我们可以修改这个这个容器内 Nginx 服务的默认文件,例如执行 vi /usr/share/nginx/html/index.html(注意这儿用的是 vi 而不是 vim,因为这个容器中默认没有安装 vim),然后按一下 i,在 Hello 后面追加一个 World,然后按一下 esc,输入 :wq,保存并退出。
再执行 exec 退出这个容器。
ok,接下来我们继续执行 docker commit 命令,后面跟上要基于的容器 ID 或容器名字,例如这儿是 ifer-nginx-v1,最后再跟上根据前面的这个容器新生成的镜像名,例如 nginx:v1.1,执行 docker images 查看,会发现果然又多出了一个 nginx:v1.1 这个镜像,同样的我们根据镜像再来生成一个新的容器,按一下上箭头,找到之前根据镜像生成容器的命令,接下来咱把 8089 改成 8088,最后这个 nginx:v1.0 改成 nginx:v1.1,这就表示基于 nginx:v1.1 这个镜像来生成一个 ifer-nginx-v2 的容器,敲回车。
最后在浏览器访问 8088,如果看到 Hello World 字样就表示根据 docker commit 命令构建镜像并启动的容器成功啦。
ok,这是关于本节课咱给大家介绍的生成镜像的第二种方式,即通过 docker commit 命令来根据容器生成镜像,谢谢大家。
发布镜像
1 | docker login -u iferdva |
自己编写的镜像如何共享给别人去使用呢?
常见的有两种方式,第一种方式是把自己的镜像发布到 Docker Hub,然后别人直接可以下载使用,第二种方式是把镜像打包成本地文件,别人拿到这个本地文件后直接进行加载就好啦。
好,咱先给大家演示一下第一种方式,如何把自己制作好的镜像发布到 Docker Hub,例如咱希望把前面制作的 nginx:v1.1 这个镜像发布到 Docker Hub(需要大家注意的是:我后面的操作都需要科学上网,如果你暂时不能科学上网,先了解或跳过即可)。
首先我们要在这个网站,hub.docker.com 上注册一个账号,我的账号的名字叫 iferdva,接下来我们可以在命令行进行登录操作,执行 docker login -u 后面跟上用户名 iferdva,敲回车,然后输入密码,敲回车。
接下来我们要基于本地的 nginx:v1.1 镜像再制作一个符合 Docker Hub 要求的新镜像,主要是镜像的名字要符合 Docker Hub 的要求,执行 docker image tag 命令,后面跟上要基于的镜像名,例如 nginx:v1.1,再往后跟上要打的新标签的镜像名字,前面要是用户名,我的是 iferdva,然后斜杠,nginx:v1.1,当然你也可以不打标签,重新构建出来一个符合这样名字要求的镜像也是可以的,前面是用户名斜杠,后面是镜像名。ok,敲回车,接下来执行 docker image push 命令,后面跟上要推送的镜像,咱这儿就是 iferdva/nginx:v1.1,敲回车,这样的话这个镜像就被推送到远端了。
有多种方式可以查看这个镜像是否推送成功,第一种方式我们可以打开 dockerhub 这个网页进行查看,找到 My Hub,然后点一下左边的 Repositories,刷新!
第二种方式我们可以通过 docker search 命令进行搜索,后面跟上 iferdva,然后回车,发现果然搜索到了咱刚才推送的 nginx:v1.1 这个镜像。
ok,最后我们再测试一下这个镜像是否可以正常使用,先把本地的 iferdva/ngingx:v1.1 这个镜像删除,然后执行 docker run -d -p 8087:80 –name ifer-nginx-v3 iferdva/nginx:v1.1,注意此时本地没有 iferdva/ngingx:v1.1 这个镜像,所以会从远端拉取。
好,我们打开浏览器,121.40.238.160:8087,如果看到 Hello World 字样就表示根据 docker push 命令推送镜像并启动的容器成功啦。
好啦,这是本节课咱给大家说的,如何发布镜像到 Docker Hub 供别人使用,谢谢大家。
打包和加载镜像
1 | docker image --help |
还有第二种共享镜像的方式,就是把需要共享的镜像打包成本地文件,别人拿到这个本地文件后直接进行加载就好啦,ok,咱同样给大家演示一下。
例如咱的需求啊还是期望把 nginx:v1.1 这个镜像共享给别人使用。
首先执行 docker image –help 查看相关命令,发现这儿有一个 save 命令,它可以保存一个或多个镜像为压缩包文件,接下来再执行 docker image save –help 看一下 save 命令的具体使用,首先它可以简写为 docker save,后面可以跟上 -o 参数,这个 -o 参数后面跟的是要输出的镜像文件路径和名称,例如我们希望输出到当前路径,就可以写 ./,例如输出的文件名字叫做 nginx-v1.1.tar,再往后跟上根据哪个镜像进行保存,例如 nginx:v1.1,如果不写这个标签默认是 latest,ok 敲回车。接下来执行 ls 命令查看一下,发现果然就生成了这样一个 nginx-v1.1.tar 的文件。
ok,然后我们就可以把这个镜像文件发送给别人,别人拿到后只需要执行 docker image load -i nginx-v1.1.tar 命令就可以加载这个镜像啦。
看到 Loaded image 表示加载成功,好啦,这就是本节课咱给大家说的,如何打包和加载镜像,谢谢大家。
关于容器
基本操作
1 | # 查看最近创建的容器 |
ok,接下来咱学习关于容器的相关知识,执行 docker container –help 查看有哪些操作,会发现相比较镜像它的命令会多很多,咱这儿会把实际开发中最常用的都给大家说到。
1. 首先我们看一下 run 命令,这个命令咱前面使用过,他说可以根据镜像创建并启动一个容器,后面跟上一些参数,例如咱前面写过的 docker run -d -p 8086:80 –name ifer-nginx-v4 nginx:v1.1,-d 表示后台运行,-p 后面跟的是端口映射,这表示将容器内的 Nginx 服务默认的 80 端口映射到宿主机的 8086 端口,从而实现可以通过访问宿主机 8086 端口来访问容器内的 80 端口服务,–name 后面跟的是要启动的容器名字 ifer-nginx-v4,如果你不写的话他会随机一个名字,最后这个 nginx:v1.1 是镜像的名字,当然也可以是镜像的 ID,表示要根据这个镜像来启动容器。
另外 docker container run 其实是 docker container create 和 docker container start 的合写,docker container create 命令是根据镜像创建一个容器,但是不会启动容器,docker container start 命令是启动一个容器,这两个命令也可以单独使用。
2. ok,如何查看有哪些运行中的容器呢?可以执行 docker container ls 这个命令查看,会发现在运行中的容器就都列出来了,另外这个命令还有其他的一些别名,执行 docker container ls –help 进行查看,会发现他有这么多的别名,其中最常用的是 docker ps 会更加简洁一些,输入 docker ps 会发现效果和前面是一样。
3. 另外如果想停止一个容器可以使用 docker container stop 或 docker stop 命令,后面跟上要停止的容器的 ID 或名字,例如我们把这个 ifer-nginx-v4 的容器停止掉,可以执行 docker stop ifer-nginx-v4,这个时候再执行 docker ps 查看运行的容器,发现 ifer-nginx-v4 找个容器就没啦,那么如何查看所有容器呢(包括停止的),可以在 docker container ls 或 docker ps 后面加上 -a 参数,这个时候就会把所有的容器都给列出来了,包括刚刚停止的 ifer-nginx-v4 这个容器。
其他容器相关的命令还有像 docker container restart 可以重启容器,docker container rm 删除停止的容器等,需要大家注意的是这个只能删除停止的容器,如果需要删除正在运行的容器,可以指定 -f 参数进行强制删除,还有像 docker container prune 可以删除所有停止的容器。
启动容器
最后还有一个比较重要的命令是 docker container exec,它可以在正在运行的容器内部执行一些命令。
就比如说 ifer-nginx-v4 这个容器,咱先把它启动起来,我们期望查看这个容器内部的目录结构,这时候就可以执行 docker container exec,另外咱这儿给大家说一下,像 docker 后面跟的 image 啊、container 啊,一般都是可以省略的,所以后面如果没有特殊情况我会更多的使用简写了。
ok,接下来咱后面跟上 ifer-nginx-v4 ls,其中最后的这个 ls 命令就是会在容器内部执行的,敲回车,现在你看到的这些文件列表就来自于容器内部,一般在容器内执行命令的时候建议加个 -t 参数,表示会分配一个伪终端,这样会对输出的内容进行格式化,更方便阅读,咱给大家试一下,回车,发现出来的就是这样一个美化后的文件列表效果。
再比如说咱期望查看容器内某个文件的内容,这时候就可以执行 docker exec -t ifer-nginx-v4 cat 例如希望查看的是 /usr/share/nginx/html/index.html 这个文件,回车,发现就是咱前面添加的 Hello World 字样。
上面的演示我们发现执行完命令后会退出容器,如果想在容器内执行命令后不退出,我们可以把最后的这个在容器内执行的命令替换为 bash 或 sh,至于是 bash 还是 sh 取决于此容器所依赖的镜像有没有安装这相应的命令,咱这儿使用 sh,敲回车,现在你看到的就是一个类似于容器内的 shell,例如这个时候我们敲几次回车,然后输入 ls 期望查看当前容器内的目录结构,但是你发现没有反应,并没有文件列表出来,也就是说输入的内容不能和这个 shell 进行交互,那么如何解决呢?
此时 ctrl c 先退出容器,按一下上箭头调出之前的命令,这时候需要加个 -i 参数,表示使用交互式终端,-i -t 也可以简写为 -it,这时候再回车,能进入容器没有问题,然后 ls 敲回车,发现这个命令就产生作用啦,此时你看到的目录就是容器内的目录,并且现在也没有退出容器,你可以在此终端操作容器内的任意文件。例如咱把 Nginx 默认的静态文件的内容改成 8086 …
需要退出容器到宿主机的话,可以输入 exit 敲回车,发现就退出啦。ok,上面的这个命令 docker exec -it,后面跟上容器 ID 或容器名,再跟上 bash 或 sh 命令,这个操作非常常用,需要大家重点掌握!
好啦,这就是本节课咱给大家说的该如何进入容器的命令,谢谢大家。
查看日志
1 | docker container logs -f 容器ID或容器名字 |
可以使用 docker logs 命令查看容器运行后的一些日志信息,后面跟的是容器的 ID 或名字,例如我们要查看 ifer-nginx-v4 这个容器运行后的一些日志信息,可以执行 docker logs ifer-nginx-v4,回车,由于容器中主要跑的是 Nginx 服务,所以现在你看到的信息就是容器中 Nginx 服务的日志信息,输入命令的时候,也可以在容器前面加一个 -f 参数来实时跟踪日志,按一下上箭,调出原来的命令,这儿补充 -f,回车,现在就是处于日志的实时监听状态。
给大家测试一下,例如咱先选中最后一行,待会啊,选中这一行下面的输出表示的就是新产生的日志信息,接下来打开浏览器,输入这样一个地址 http://121.40.238.160:8086,为了方便待会观察,后面再拼上一个虚拟路径吧,例如 /xxx,敲回车,回到 XShell,这时候你就看到了下面两行信息的输出。
第一行是错误日志(Error log),第二行是请求日志(Access log)。先看一下这个错误日志,例如这儿表示错误发生的时间,需要大家注意的是这个时间是国际 UTC 时间,比北京时间慢了八个小时,所以在这个时间基础上加 8 就是现在你系统上的时间,当然我们也有办法让这个时间和系统时间保持一致,就是在启动容器的时候可以使用 -e 参数,通过指定环境变量的方式来指定时区,例如我们期望容器内的时区为北京或上海时区,咱这儿在重新启动一个容器给大家测试一下。执行 docker run -d -p 8085:80 --name ifer-nginx-v5 -e TZ=Asia/Shanghai nginx:v1.1,敲回车。
好,现在又启动了一个新容器,我们通过 docker logs -f,后面跟上 ifer-nginx-v5,选中一下最后的信息,打开浏览器,把这儿改成 8085,敲回车,回到控制台发现这儿日志的时间就是北京时间啦,和你系统上的时间是完全一致的,这就是我们通过 -e 参数指定时区环境变量后达到的效果。
ok,最后咱再给大家简单说一下这两行日志的整体含义,这个意思是说 IP 为 27.18.155.157 的客户端通过 Chrome 浏览器,访问了 121.40.238.160:8085 的这个服务,请求方式是 GET,请求的地址是 /xxx,HTTP 协议的版本是 1.1,但 Nginx 在默认网站根目录 /usr/share/nginx/html/xxx 下找不到相关文件或目录,因此返回了 404 错误。
ok,咱也可以给大家看一下客户端 IP 是不是确实就是 27.18.155.157 这个地址,打开百度输入 IP,敲回车,发现和这儿是完全一样的么有问题。
好,这就是本节课咱给大家说的该如何查看和分析 Docker 容器中的日志信息,谢谢大家。
1 | 2025/09/17 01:59:13 [error] 22#22: *368 open() "/usr/share/nginx/html/xxx" failed (2: No such file or directory), client: 27.18.155.157, server: localhost, request: "GET /xxx HTTP/1.1", host: "121.40.238.160:8085" |
其他小命令
1 | docker container stats # 查看所有运行的容器状态 |
其他和容器相关的命令还有像 docker stats,后面可以跟上一个或多个容器的 ID 或名字,表示查看对应的容器运行状态,如果后面什么都不指定的话,表示可以查看所有容器的运行状态,例如每个容器 CPU、内存使用、或网络 IO 等资源的消耗情况,我们根据这个输出信息啊,从而可以进行有效的性能优化或者说故障排查等操作。
ok,还有像 docker container inspect,这个也比较常用,后面跟上容器的 ID 或名字,可以查看容器的详细信息,例如 ifer-nginx-v5,这里面信息会有很多,我们后面用到的时候再具体去说。
还有一个命令,像 docker cp 也会经常用到,它可以在容器和宿主机之间进行文件的相互复制,它的语法是这样的,例如我们希望把 ifer-nginx-v5 这个容器内的 Nginx 首页文件复制到宿主机,就可以这样写 docker cp 8ca:/usr/share/nginx/html/index.html ./,再比如我们期望把宿主机的某个文件复制到容器的某个目录,例如我们先在宿主机 123.txt,通过 echo 命令添加一些内容,接下来执行 docker cp,把 123.txt 这个文件复制到容器 8ca:/usr/share/nginx/html 的这个目录,ok,进入容器进行查看 …
好啦,这就是本节课咱给大家说的关于容器的比较常用的三个小命令,docker stats、docker inspect 以及 docker cp,谢谢大家。
数据管理
数据卷
在 Docker 中有一个很重要的概念需要大家掌握,数据卷。数据卷可以理解为容器与宿主机之间的一个 “数据共享的仓库”。在容器运行过程中,会产生一些数据,比如日志文件、数据库文件等。这些数据如果只存储在容器内部,当容器被删除时,数据也会跟着丢失。而通过数据卷允许你将容器内的数据存储到宿主机指定的目录中,这样即使容器被删除,数据卷中的数据啊也能得到保留。同时,你还可以在多个容器之间共享同一个数据卷,实现数据的复用。
ok,使用数据卷呢有两个大的步骤,第一步呢,先使用 docker volume create 命令创建数据卷,第二步呢,在启动容器的时候进行数据卷和容器目录的关联。例如咱的需求是:新启动一个容器,把容器中 Nginx 默认的静态资源目录挂载到宿主机。
1 | # 创建数据卷 |
1 | [ |
ok,根据刚刚说的第一步先创建数据卷,后面跟的是数据卷的名字,例如咱这儿叫 volume-nginx-html,接下来执行 docker volume ls 可以查看数据卷有没有创建成功,发现就存在了这个数据卷,要想继续查看这个数据卷的详细信息可以执行 docker volume inspect 后面跟的是数据卷的名字,例如 volume-nginx-html,这儿出来的是一个数组,应该来说 inspect 的时候后面可以跟多个卷名来查看多个卷的详细信息。
ok,这儿有好多和卷相关的信息,咱平常关注最多的是这个 Mountpoint,它表示数据卷在宿主机上的实际物理存储路径,当容器挂载此卷时,Docker 会将容器中对应的数据持久化到该目录。
ok,接下来咱们就进行第二打不,创建容器并挂载数据卷,执行 docker run -d -p 8084:80 -v 后面跟的是卷名和容器内目录的映射,例如刚刚创建的 volume-nginx-html:/usr/share/nginx/html,然后是 –name 后面跟的是容器名,例如 ifer-nginx-v6 然后还是基于 nginx:v1.1 这个镜像来启动容器。
敲回车,此时就创建并启动了一个容器,通过 docker ps 命令可以查看,浏览器打开 8084 的这个地址也可以查看,没有问题。
接下来咱给大家做一些测试,测试卷对应的宿主机的目录内容或文件发生变化后,容器确实会跟着变化,容器内的内容变了,宿主机对应的这个目录也会跟着变化。
好,cd 到卷对应的这个目录,/var/lib/docker/volumes/..,ls,发现这儿有两个文件,这是因为上面的映射操作会自动把容器内的文件同步到卷对应的这个目录,ok,此时我们执行 vim 修改一下 index.html,例如把内容改成 8084。接下来执行 docker exec -t ifer-nginx-v6 cat /usr/share/nginx/html/index.html 查看容器内的这个文件有没有跟着变化,敲回车,发现也发生了变化,当然打开浏览器刷新页面也是没有问题的。
接下来我们再修改容器内的这个文件,看一下宿主机也会不会跟着变化。执行 docker exec -it ifer-nginx-v6 vi /usr/share/nginx/html/index.html,再退出容器,进入到卷对应的目录,cd /var/lib/docker/volumes/volume-nginx-html/_data,cat index.html 查看文件,发现也是保持同步的,刷新页面也是没有问题。
ok,这是关于数据卷对应的宿主机目录和容器目录的双向同步的演示。
还有一点,这个时候即使我们把这个 ifer-nginx-v6 容器给删除了,volume-nginx-html 这个卷以及对应的目录也还是存在的,因为这些它是存在于宿主机的,和容器进行了剥离,这个我就不再演示了。
好啦,这是关于容器中数据卷的使用,咱先给大家说到这儿,谢谢大家。
直接挂载主机目录
1 | # docker run -d -v <host-path>:<container-path> --name <container-name> <image-name> |
1 | docker ps |
ok,接下来咱再给大家说一下容器中数据管理的第二种方式,这种方式无需创建数据卷,在启动容器的时候,也是通过 -v 参数直接进行宿主机目录和容器目录的关联。
例如咱的需求还是:启动一个新容器,把 Nginx 容器中默认的静态资源目录挂载到宿主机。
直接执行 docker run -d -p 8083:80 -v,注意这个时候后面跟的直接是宿主机的目录,例如 ~/nginx/html,如果宿主机没有这个目录它会自动创建,接下来冒号,容器内的目录就是 /usr/share/nginx/html –name ifer-nginx-v7 nginx:v1.1
ok,接下来打开浏览器给大家看一下效果,最后的端口改成 8083,会发现出现了 403,嗯这是为什么呢?
其实原因啊,是因为这样直接挂载宿主机目录的方式,它会用宿主机的这个目录覆盖容器内的目录,而宿主机刚开始创建出来的这个目录是空的,所以你查看容器内的目录话呢就也被覆盖为了空目录,例如给大家看一下,docker exec -t ifer-nginx-v7 ls /usr/share/nginx/html,发现这儿是空的,根据 Nginx 的访问机制,这种情况下就会出现 403。
细心小伙伴会发现,这种直接挂载目录的方式和前面学习的先创建 Volume 再挂载的方式有一个明显的差异,那就是,先创建 Volume 的方式会自动把容器内的数据复制到卷对应的宿主机目录,而这种直接挂载目录的方式则会把宿主机的这个目录内容覆盖容器内的目录内容,这一点大家需要特别注意。
其实核心原因是因为这两种方式的设计初衷是不同的,Volume 是通过 Docker 管理的存储:设计初衷是为了安全持久化容器中的数据,因此会自动把容器内的数据复制到卷对应的宿主机目录,避免数据丢失。而这种直接通过宿主机目录挂载的方式是用户自己负责管理的存储:设计初衷是为了灵活注入外部数据(如复杂的配置文件,或者说代码等信息),因此会直接覆盖容器内的内容。简单来说:volume 是 “容器数据优先,自动持久化”;目录映射是 “宿主机数据优先,用户自主管理”,当然最终的效果两边的修改都会保持同步,只是在启动容器的时候有这样一个差异。
ok,那么该如何解决这个问题呢?
第一种方式当然可以进到宿主机目录 ~/nginx/html,创建一个 index.html 文件,但有的时候这个文件的内容可能会比较复杂,例如 Nginx 的配置文件往往咱也希望挂载的宿主机,这个时候你去重头到尾创建这个配置文件就有一些麻烦了。所以我推荐使用前面学习的 docker cp 命令,复制其他已存在容器内的内容到宿主机,然后再次基础上进行修改,例如执行 docker ps 看一下有哪些运行的容器,例如 docker cp 8ca:/usr/share/nginx/html/. . 这样一个命令把这个容器内这个目录下的所有文件都拷贝到宿主机的当前目录了,例如我把 index.html 中内容改成 8083。
最后还有一点咱给大家做一个说明,就是一般情况下啊,我们应该先执行 docker cp 命令从其他容器内拷贝过来相应的内容,然后再根据这个已有的内容目录或文件挂载并启动容器,这样的顺序才是最正确的,尤其是关于配置文件的挂载,否则可能出现容器或容器内的服务无法正常启动的情况。
ok,最后咱打开浏览器给大家印证一下,刷新,就看到了对应的内容。好了,这就是本节课咱给大家说的容器内数据管理的第二种方式,即启动容器的时候直接挂载宿主机目录,谢谢大家。
关于网络
Docker0 Bridge
Docker 中有三种类型的网络,可以通过 docker network ls 进行查看,分别是 bridge 桥接网络、host 主机网络和 none 没有网络。咱先给大家介绍第一种也是最常用的一种 bridge 桥接网络。
Docker 安装并启动后,会在宿主机中添加一个名为 Docker0 的网桥,可以通过 ifconfig 命令查看,这个 Docker0 就属于 Bridge 类型,它承担着容器与宿主机,以及容器与容器之间的网络通信。
下面是 docker0 网桥的详细信息,其中这儿有个 inernet 地址 172.17.0.1 就是该网桥的 IP 地址,同时也是连接到此网桥的容器的网关地址,默认情况下,我们前面通过 docker run 运行的所有容器都挂载在这个 docker0 网桥上。
ok,这个 NAME 叫 bridge 的网络对应的就是默认的 docker0 桥接网络。也可以通过 docker network inspect bridge 命令查看这个桥接网络的更加详细的信息。例如这列出了该网络的网关和子网,所有连接到此网络的容器会从这个网段分配 IP。
接下来咱找到 Containers 字段,这里列出的就是挂载到此网桥的所有容器,通过名字看到就是我们前面创建的那些容器,每个容器都有自己的 IPv4 地址(这些地址都来自于上面的这个子网 172.17.0.0/16),这些 IP 地址都是在同一个网段,所以相互之间可以进行通信,待会咱会给大家进行演示。
也可以通过 docker inspect 后面跟上具体的容器 ID 或名字,来查看对应容器内的网络信息。例如 ifer-nginx-v1,咱找到 Networks 字段,也是可以看到这个容器的 IP 地址的,网关地址 127.17.0.1,其实这个网关地址就是 docker0 网桥的 IP 地址。
ok,接下来咱就给大家演示一下宿主机与容器,以及容器与容器之间的网络通信。好,先给大家演示一下,宿主机和容器之间的网络通信,先看一下这些容器的 IP,执行 docker network ls,然后 docker network inspect bridge,找到 Containers 字段,可以看到这些容器的 IP 了,然后在宿主机执行 curl 172.17.0.2,敲回车,会发现拿到了容器内对应服务的内容了,没有问题…
好接下来我们进入某个容器,演示一下这个容器与其他容器之间的通信,执行 docker exec -it ifer-nginx-v1 sh,进入容器,执行 ping 172.17.0.5,首先能 ping 通没有问题,或者 curl 172.17.0.5 也能拿到内容,没有问题 …
需要大家注意的是,像 172.17 这种网络地址是私有地址,不能在公网中访问,所以容器内的服务要想被外部网络访问,还是要通过咱前面一直在使用的 -p 端口映射的方式来实现。
好了,这就是本节课咱给大家说的关于 Docker 网络的第一种形式,Bridge 桥接网络,谢谢大家。
1 | docker network ls |
Custom Bridge
1 | docker network create ifer-network1 |
除了默认的 docker0 bridge 桥接网络,我们还可以创建自定义的桥接网络来连接各个容器,相比较默认的 docker0 bridge 来说,自定义的 bridge 桥接网络提供了 DNS 服务,它可以将容器名称解析为 IP 地址,所以容器与容器之间可以通过容器名称进行网络通信,这一点会比较有用,因为 Docker 容器可能是频繁创建和销毁的,那么 IP 呢也就会随之变化,所以使用 IP 通信就没有那么稳定,而容器名称在创建的时候是可以直接进行指定的,所以会比较方便,接下来演示一下它的使用。
首先创建网络,通过 docker network create 后面跟上名字,例如 ifer-network1,默认情况下这个网络就是桥接类型的网络,可以通过 docker network ls 来进行查看,会发现我们新创建的这个网络它确实也属于 bridge 类型,接下来我们启动新的容器并加入这个自定义网络,执行 docker run -d 如果这个容器内的网络你不希望外部访问的话,可以不用 -p 映射宿主机端口,接下来 –name ifer-nginx-a,再往后 –network 来指定刚刚自定义的网络 ifer-network1,再往后是镜像的名字 nginx:v1.1。
ok,为了演示使用自定义网络的容器间的通信,咱再创建一个新容器,也是连接到刚刚的创建的这个自定义桥接网络,按一下上箭 …
好啦,接下来咱测试一下这两个容器之间是否可以通过容器名进行通信,首先进入其中一个容器,执行 docker exec -it ifer-nginx-a sh,进入容器,执行 ping,注意这一次,后面咱可以直接跟上容器的名字,因为刚刚说了自定义的桥接网络自带 DNS 解析的服务,它会把容器名字解析到这个容器的 IP,这儿写 ifer-nginx-b,能 ping 通,然后 curl,也能拿到内容,没有问题。
好的,这是本节课咱给大家说的该如何自定义桥接类型的网络,这种在实际工作中啊,也是使用最多的,需要大家重点掌握。
Host 和 None
最后还有其他两种模式的网络,分别是 host 和 none,咱也分别说一下。
首先是 host,通过 host 网络模式启动的容器,它没有一个独立的网络空间,而是完全使用宿主机的 IP 和端口,这种模式最大的好处是性能,它的一个典型应用场景是需要容器与宿主机共享网络资源,例如,如果需要在容器内部直接访问宿主机的网络端口或 IP 地址,来实现网络监控功能,此时就可以使用 host 网络模式。他的语法是这样的:
1 | docker run -d --net=host --name ifer-nginx-v8 nginx:v1.1 |
敲回车之后,你就可以通过浏览器访问宿主机的 80 端口,就可以拿到容器内的内容了。前面我已经通过这种方式启动了一个 nginx 容器,就是这一个,可以通过 docker inspect mynginx 来查看容器的详情,可以看到就是 host 模式,所以宿主机的 80 端口已经被这个容器占用了,那我这儿就不再敲回车进行演示了。
最后需要大家注意的是,该模式下,容器网络的隔离性会比较差,一般需要谨慎使用。
还有最后一种 none 表示没有网络,它将容器与宿主机隔离开来,这意味着容器不能访问外部网络,也不能被外部网络所访问,只能在容器内部借助自带的回环网络 loopback 自己与自己通信,使用的语法就是,启动容器时通过 –net=none 这个参数,它适用于不需要联网的应用,这种平常使用相对也比较少。
好啦,这就是本节课咱给大家简单介绍的 host 和 none 两种网络模式,谢谢大家。
Dockerfile
基础
接下来学习 Dockerfile。
如果说 Docker 镜像是一个程序的安装包,而 Dockerfile 就是这个安装包的“制作说明书”,Dockerfile 是一个文本文件,里面包含了一系列的指令,这些指令会按照顺序从上到下执行,最终构建出一个完整的镜像。接下来咱以部署一个前端项目为例给大家进行讲解 Dockerfile 的基础使用。
首先咱先创建一个前端项目,可以通过 npm create vite@latest 命令,需要大家注意的是,使用 npm 前,要保证你的机器已经安装了 Node,我的 node 的版本是 22.18.0,接下来敲回车,首先是项目名字,框架咱就选择 Vue 吧,接下来一路回车,耐心等待,好,这样就启动了这个项目,在外部浏览器咱可以通过输入 IP,然后,5173 来查看一下这个应用,发现并不能访问,原因其实这儿也提示了,咱需要通过 –host 来暴漏出去,所以咱 cd 到 vite-project 目录,执行 vim package.json,在 dev 这儿,vite 后面加上 –host,esc,冒号,wq 保存并退出,再次执行 npm run dev,打开浏览器刷新,发现就可以看到这个应用了,咱接下来的目标啊,就是通过创建 Dockerfile 来实现这个应用的部署,最终也能看到这样一个效果。
首先通过 vim Dockerfile,创建这样一个文件,接下来我粘贴过来一些指令,咱详细给大家解释一下每一条指令的含义。
1. 首先是 FROM 指令,后面可以跟上具体的镜像,后续再使用的指令都会基于此镜像所提供的环境来运行,我们在构建镜像的时候,Docker 引擎会先看一下本地有没有 FROM 后面的这个镜像,如果有就直接使用,如果没有则会去远端拉取。需要大家注意的是 Dockerfile 文件中的 FROM 是必备指令,一般情况下第一条指令就是 FROM,后面的 as 这些先不用管,我们待会再说。
2. 接下来是 WORKDIR 指令,后面可以跟上具体的目录,后续使用到的指令都会在当前目录下执行,如果没有此目录会被自动创建。
3. 接下来是 COPY 指令,后面可以跟上一个或多个宿主机的源路径,中间用空格隔开,现在这儿是一个相对路径,它相对的是构建镜像上下文,在 Docker 中,构建上下文(Build Context)是指执行 docker build 命令时,传递给 Docker 引擎的本地文件目录,例如待会我们构建镜像的时候会执行 docker build -t my-image .,其实构建上下文就是这个 . 目录,可以通过 pwd 命令查看是具体是哪个目录,其实就是这个目录。
ok,最后一个表示目标路径,这个目标路径如果是一个相对路径,则相对的是 WORKDIR 指令指定的目录,对于咱这儿写的 . 其实就是 /app 目录。
COPY 还有另外一种写法,后面跟上的是一个数组,前面的这些还是表示源路径,最后一个元素是目标路径,这种写法和上面的区别是,如果你拷贝的文件名中包含空格,例如 a 空格 b.txt 这种,那么这种写法会比较合适。
4. 再往下是 RUN 指令,用于在构建镜像的时候执行一些 linux 命令,例如我们这儿是执行 npm i 安装 package.json 中的所有依赖。
5. 接下来这儿又是拷贝,这表示把构建上下文中的所有内容都拷贝到目标路径,后面这个 . 就表示前面通过 WORKDIR 指定的 /app 目录,但其实这样做还会有一个问题就是它还会把宿主机的 node_modules 给拷贝过去,这显然不是我们期望 ,因为前面我们通过 npm i 已经安装好了所有依赖,那么如何解决呢,这时候我们可以新建一个 .dockerignore 文件,这个文件中可以指定一些文件或目录不参与构建,例如我们可以添加 node_modules,这样在执行构建相关命令的时候就会忽略 node_modules 啦。
6. 接下来又是 RUN 指令,表示执行 npm run build 进行打包。
7. ok,接下来这个阶段做个事情就是把前面打包的内容放到 Nginx 服务的指定目录。
8. 再往下还有个 EXPOSE 指令,用于声明容器监听的端口,这个端口是给镜像使用者看的(没有其他实际作用),例如我们一看就会知道这个容器内的服务对应的是 80 端口。
9. 最后的这个 CMD 指令,表示运行容器时默认被执行的命令,咱这儿的意思是说启动 Nginx 服务,每个 Dockerfile 中只能有一条 CMD 指令,如果有多条,则只有最后一条会生效,其实它也支持非数组 Shell 格式的写法,咱这儿就先不说,一般还是更推荐这种数组的形式。
1 | FROM node:22-alpine AS build-stage |
ok,接下来我们执行 docker build 命令来构建镜像,通过 -t 后面跟的是新构建出来的镜像名,例如我这儿叫 my-react-app:v1.0,然后空格 .,这个点表示指定构建上下文路径,它会从这个路径自动查找 Dockerfile 文件进行构建,那如果你的文件名不叫 Dockerfile 的话,需要通过 -f 参数来指定具体的文件名。
ok,敲回车,接下来执行 docker images 查看镜像,发现就多了一个新构建出来 my-react-app:v1.0,接下来我们根据这个镜像启动容器试一下,执行 docker run -d -p 8082:80 –name ifer-nginx-v8 my-react-app:v1.0,然后打开浏览器访问 8082 端口,此时你看到的就是根据 Dockerfile 构建出来的镜像所启动的容器服务。
好啦,这就是本节课咱给大家介绍的 Dockerfile 的基本使用,谢谢大家。
镜像的分层设计
在项目迭代的过程中,Dockerfile 对应的资源会经常被修改,因此需要频繁的重新构建镜像,Docker 为了提高镜像的构建速度,采用了分层设计方案,所谓分层设计,就是将一个完整的镜像分解为多个只读的层,Dockerfile 中的相关指令会生成对应的构建层,例如 COPY、RUN、ADD 等指令,这个层会记录该指令所做的文件系统相关的变更,例如新增文件、修改文件或删除文件,每一层都有一个唯一的哈希值作为其标识符,下一次再构建时,Docker 根据这个哈希值来判断镜像层的内容是否发生变化,如果没有变化,则直接使用该层的缓存,如果变化,则重新构建该层信息。
这样做的好处是,会大大的提高镜像的构建速度。
如果说咱现在什么都不改,再构建一个 my-react-app2 的镜像,这时候速度会非常的快,因为在构建镜像的时候它完全会从缓存加载每一层的数据,例如咱执行 docker build -t my-react-app2 . 命令给大家看一下效果,敲回车,你会发现左边的 COPY RUN、COPY RUN 这些操作都走了缓存,Cached,所以,每一步操作的右边几乎没有耗时的操作。
只有第三行和第四行的操作花了一点点时间,这表示 Docker 进行了一个远程检查,以确保使用的基础镜像不是过时的。注意这只是检查,不代表重新下载,如果检查到本地的这个镜像信息和远端是一致的,Docker 还是会直接使用本地缓存。
ok,实际开发中,我们并不需要构建一个完全一样的镜像,例如我们修改了一些代码,看一下修改代码之后再次构建镜像的速度是怎样的。例如此时我们把首页的这个 Vite + React 文字改为 WPS,vim …在构建镜像之前咱看一下之前的 Dockerfile,给大家先分析一下缓存的名字情况是怎样的。例如咱就以这几个指令 COPY、RUN、COPY、RUN 为例给大家进行说明:
在前面构建镜像的时候,这儿其实已经形成了 4 个层。
1 | WORKDIR /app |
ok,由于刚刚我们修改了源代码,咱再分析一下再次构建镜像的时候这几个层会不会走缓存。
首先是层 A(COPY package.json …):因为 package.json 文件的内容未发生变化,所以会复用缓存。
接下来是层 B(RUN npm i):由于上面 package.json 中的依赖未变,所以这一步也会复用缓存(无需重新安装 node_modules),会大大的节省时间。
再往后是层 C(COPY . .):这里会对所有的源代码进行拷贝操作,由于我们的源代码已经发生了变化,所以这一步不会走缓存。一旦这一步没有走缓存,则当前阶段后面的所有步骤的缓存都会失效(至于当前阶段是什么意思,咱后面再说)。
ok,咱再次执行 docker build -t my-react-app3 . 给大家看一下效果,耐心等待,会发现层 A 和层 B 走了缓存,而层 C 就没有走缓存,通过右边咱也能发现这一步确实也产生了耗时,包括当前阶段后面的这一步也没有进行缓存,和咱之前分析的效果是一致的。好啦,这是关于咱对镜像分层设计的一些简单分析,谢谢大家。
思考
接下来请大家思考一个问题,就是我们前面编写的 Dockerfile,先是 COPY 了 package.json 和 package-lock.json 到这个 /app 目录,再往后呢又进行了 COPY 点点的操作,也就是又拷贝了当前目录下的所有文件到这个 /app 目录,所以!感觉是不是可以把这两步合并成一步,直接把这一行改成 COPY . .,然后下面这一行删掉呢,大家可以停下来思考一下像现在这样编写 Dockerfile 是不是 ok 的?
1 | FROM node:22-alpine AS build-stage |
ok,虽然这样也能构建镜像成功,但其实非常不建议这样写,因为任何内容的变化都会走 COPY 点点,从而导致当前阶段,这个指令后面的所有缓存都会失效,即便我们也只是改了源代码,没有涉及到 package.json 文件的变化,后面这个重新安装依赖的操作也会执行,而这一步也是最耗时的。所以,看似我们节省了一步操作,其实却带来了比较大的隐患。这儿呢,咱也给大家说一条编写 Dockerfile 时候的一个小小的原则:那就是低频指令前置。
什么意思呢?就是将变化频率低的指令放在前面(例如只有安装依赖才会变化的 package.json),变化频率高的指令放在后面(例如这个代码的复制操作,因为任何内容的变化都需要我们重新进行拷贝),这样呢,可以最大化的进行缓存复用。
好啦,这就是本节课咱给大家说的编写 Dockerfile 时候的一个小的技巧:即低频指令前置,这样可以尽可能利用缓存,提升构建镜像的速度,好,谢谢大家。
多阶段构建
多阶段构建是 Docker 提供的一种优化镜像体积的技术,核心是通过在一个 Dockerfile 中定义多个构建阶段(每个阶段通过 FROM 指令开始),最终只保留运行所需的最小环境和产物,从而大幅减小最终镜像的体积。
以前面我们编写的 Dockerfile 为例,我们可以将其拆分为两个阶段来理解:
第一阶段是第一个 FROM 这儿,它将这个阶段通过 AS 关键字命名为 build-stage,
1 | FROM node:22-alpine AS build-stage # 定义第一个阶段,命名为 build-stage,基础镜像是 node 环境 |
作用:
这个阶段的目标是生成应用的可运行产物(比如前端项目的 dist 目录)。
- 依赖
node环境是因为需要执行npm install(安装依赖)和npm run build(打包)等命令; - 这个阶段会包含大量冗余文件(如
node_modules依赖目录、源代码、构建工具等),这些文件在最终运行时是不需要的。
第二阶段:运行阶段(最终镜像)
1 | FROM nginx:alpine # 定义第二个阶段,基础镜像是 nginx 环境(轻量的 web 服务器) |
作用:
这个阶段的目标是运行应用,只保留必要的运行环境和产物。
- 依赖
nginx环境是因为前端静态文件需要通过 web 服务器(如 nginx)提供访问; - 通过
COPY --from=build-stage指令,仅将第一阶段生成的dist目录(构建产物)复制到当前阶段,完全抛弃了第一阶段的node_modules、源代码等冗余文件。
多阶段构建的核心优势
减小镜像体积:
最终镜像仅包含nginx环境和dist产物,而第一阶段的node环境、依赖目录等都不会被包含,镜像体积会大幅减小(通常能减少 70% 以上)。减少冗余和安全风险:
源代码、构建工具、依赖包等敏感或不必要的文件不会出现在最终镜像中,降低了泄露风险,也减少了攻击面。简化 Dockerfile 维护:
无需手动编写脚本清理冗余文件,通过多阶段自然分离“构建”和“运行”两个过程,逻辑更清晰。
总结来说,这个 Dockerfile 通过两个阶段完成了“前端项目构建 →nginx 部署”的全流程:第一阶段用 node 环境生成构建产物,第二阶段用 nginx 环境运行产物,最终得到一个轻量、安全的可运行镜像。
你无法通过 docker history 看到 build-stage 的层,是因为多阶段构建的中间产物在最终镜像中被丢弃了。但是,Docker 的本地构建器会默默地记录并维护这些中间阶段的缓存信息。当再次构建时,它通过匹配 Dockerfile 指令和文件校验和,来智能地判断是否可以复用这些本地缓存,从而实现快速增量构建。
容器层
我们前面根据镜像所启动的容器,其实本质上来说也是在顶层添加了一个可写层。当多个容器基于同一个镜像运行时,它们会共享这个镜像的所有只读层。这样可以大大节省磁盘空间,并加速部署。所有在容器运行时产生的文件系统修改(如创建、修改或删除文件)都会记录在这个可写层中。写时复制: 当容器需要修改一个只读层中的文件时,Docker 并不会直接修改该文件,而是会将该文件从只读层复制到可写层,然后再对可写层中的副本进行修改。这个过程就是“写时复制”。
删除操作: 如果在容器中删除了只读层中的文件,Docker 并不是真正删除文件,而是在可写层中创建一个“删除标记”(whiteout 文件)。当联合文件系统合并时,这个标记会掩盖底层只读文件,使得用户看起来这个文件被删除了。
mark
node:22-alpine 和 nginx:alpine 都属于基础镜像,它们的缓存逻辑是相同的。之所以 node:22-alpine 那行没有明确的 CACHED 标签,而 nginx:alpine 有,是 Docker 构建器(BuildKit)在输出上的一个细微差别,并不代表 node:22-alpine 没有使用缓存。这通常发生在基础镜像在本地已经存在,但需要先进行一次元数据检查的情况下。
load metadata:这个步骤是 Docker 检查远程仓库以确保本地镜像版本没有过期,它会耗费一点时间。
FROM 语句:一旦 load metadata 完成,Docker 确认本地缓存有效,就会在 FROM 这一步立即使用本地缓存,所以耗时为 0.0s。
如果 Docker 必须重新下载 node:22-alpine,那么 FROM 这一行的耗时会和 load metadata 的时间合并,并且会显示下载进度条和解压过程
测试
改成 WORKDIR /xxx 之后,当前阶段,这个指令下面的缓存都会失效