Docker 终极入门教程-上篇

Docker 安装

其他 OS 的安装在 Docker 官网都能找到,就不占篇幅了,这个自行处理,如果有问题可以留言讨论。这边特别提醒一下,本文的示例都是在 Ubuntu 上完成的。

安装完成之后,如果想要让非 root user 也能用 docker 命令,也就是不用在每次执行 docker 指令前都加 sudo 的话,记得执行下列指令,可以将你目前的使用者加入 docker 群组中,已获得执行的权限:

php 复制代码
# your-user 记得换成自己的使用者名称
# 执行完成后,退出该使用者,再重新登录一次让命令生效
$ sudo usermod -aG docker your-user

安装完成后,如果要检查安装有没有成功,可以用以下这个指令检查看看版本:

css 复制代码
$ docker --version
Docker version 24.0.5, build ced0996

或是执行以下两个命令来测试:

php 复制代码
# Show the Docker version information
$ docker version
# Display system-wide information
$ docker info

Docker 基本组成

上面这张图算是概括了 Docker 的重要组成与指令了,我们先来看看 Docker 中重要的三个最基本的东西:

  • image: 这是一个只读 的模板,可用来创建 container,如果有学过面向对象语言,可以想成 image 就是 class、container 是 object 或是 instance。

  • container: 这就是我们的主角,是一个独立、隔离的空间,里面包括了我们运行一个应用程序所需要的组件。

  • Docker registry: 这其实就很像 github,里面存放了很多 images,公开的像是 Docker Hub ,你也可以创建自己私人的 registry。

我相信这样说完之后,没概念的还是没概念,就让我们直接启动一个容器来看看吧!

Docker 指令: 启动容器

在正确安装 docker 后,执行此指令:

shell 复制代码
$ docker container run -it node:20 /bin/bash

应该会看到以下的画面:

  • docker container run :启动一个新的 container。

  • -i : --interactive 启动交互模式,保持标准输入打开,通过终端窗口与容器内部的进程进行实时交互操作。

  • -t : --tty 让 Docker 分配一个虚拟终端(pseudo-TTY),并且绑定到容器的标准输出上。

  • node:20 : 启动这个 container 所使用的 image。

  • /bin/bash :容器启动后要执行的命令。

简单地说,以上指令就是我们「从 image node:20 启动了一个 container,并且开启了它的输入/输出,然后让它执行 /bin/bash 这个命令」。

在执行完这行指令后,你可以看到下图所示的画面,其中 4c9bee8fef4b 为容器 ID 用于区分不同的容器实例,每次启动新的容器都会不同,要注意的是,这时候我们已经不在我们原本的环境中了,而是「进入到」了 container 中,可以在这里执行 node -v 指令,会发现这个环境中已经安装了 node,且版本是 20.5.1。

这时候我们在 container 中执行 ps aux 会看到它 PID 为 1 的 process 就是我们刚刚指定的 /bin/bash ,这个非常的重要,但我们要放到后面再讨论了。

这里让我们先做个小测试:

  1. 在刚刚创建的容器中,创建一个文件,例如 touch AAA.txt ,建议完成后用 ls 确认一下。

  2. 开启另外一个命令窗口,同样再执行一次 docker container run -it node:20 /bin/bash ,然后一样进入了一个容器,执行 ls 看看,应该会看不到 AAA.txt 这个文件。

这里很重要的就是我们提到的 container 是一个独立的、隔离的环境,当你启动了两个容器时,即便是从同一个 image 启动起来,Docker 也是会帮你创建出两个不同的容器的:

这时候再开启一个新的命令窗口,然后执行以下指令:

shell 复制代码
$ docker container ls

这个指令是用来列出目前有哪些 container 在运行中,执行后应该可以看到:

这里看到目前在环境中已经启动了两个容器,而你的 CONTAINER ID 与 NAMES 会跟上面图片的不同。

Docker Image

在更近一步讨论容器的其他操作之前,我们先来想一下, node:20 这个 image 是哪里来得呢?

不知道你有没有注意到,我们刚刚启动了两个容器的执行过程其实不太一样:

启动第一个容器的画面:

启动第二个容器的画面:

第一个容器启动时会多了很多东西,第一行是 Unable to find image 'node:20' locally ,这句话的意思是在「本地找不到这个 image」,所以接下来就会开始 pull from library/node ,也就是去 registry 拉取我们所指定的 image,没有指定的话,通常就是去 Docker Hub 拉,例如 https://hub.docker.com/node),从这个页面我们可以发现 Docker Hub 提供了不同版本的 node 环境,而我们刚刚用的是 20 这个版本。

而当我们启动第二个容器的时候,因为本地已经有了 node:20 这个 image,所以就可以直接启动,而不用再去拉取 image 了。

Docker Hub 上有各式各用的 image,非常地好用,例如你想要启动一个 MySQL,但不想装在自己的电脑上,这时候你就可以从 Docker Hub 上拉取一个 MySQL,还可以随时换不同的版本。不过这边建议尽量用官方提供的 image 比较好,毕竟你不知道别人提供的 image 里放了什么东西...

在启动容器之前,也可以先用指令把 image 拉取好,例如 $ docker image pull node:18 ,这就是要从 Docker Hub 上拉下一个 tag 为 18 的 node image 到本机来。

这边补充一下 tag,docker image 的名称中 : 前面的是名称,后面的是 tag( image_name:tag_name ),tag 通常用来表现一些特殊的信息,例如版本,像 node:20 ,就是版本为 20 的 node image。当然,既然叫 tag,不是叫 version 什么的,就表示它不一定是要用来标注版本,基本上就是留一个栏位让你来标注一些信息,而且这是非必要的、可以不用放,例如 docker image pull node ,当没有放 tag 时,它会自动帮你拉 node:latest 这个 image,也就是说, latest 是默认的 tag。

如果想要看看自己的环境中目前有哪些 image,可以用以下指令:

shell 复制代码
$ docker image ls

这边可以看到,两个 image 的大小分别都有 1 G 多,虽然这不是这两个 images 真正占用的硬盘空间,但如果你使用的 images 都比较没有关联,那日积月累下来,还是很占硬盘空间的。所以没事不要乱拉,或是要定期清理,才不会占用太多硬盘空间喔,之后再来分享怎么清理。(虽然我们后面会讨论到 image layer 共用的部分,但积少成多,在主机中放了一堆 images,硬盘还是会被撑爆的...)

Image Layer

刚刚在说到 image 时,我们说 image 是一个「 只读」 的模板,这是什么意思呢?还记得我们刚刚做的测试吗?在第一个 container 中新增了一个 AAA.txt,但后来启动的第二个 container 中并没有看到这个文件。

这是因为当我们从 image 启动一个 container 时候,Docker 会载入这个 image 作为只读层,并且在上面加上一个可写层,而我们在这个 container 中的操作实际上就是发生在这个可写层中:

不管你在这个可写层中做了多少事情,例如在这个 container 中加入一个新的文件,这个文件是存在这个独立的可写层中的,当你下次又从 node:20 这个 image 来创建另外一个新的 container 时,仍会用原本蓝色这个区块(原本的 node:20 )来建构这个 container,并且在上面加上另外一个可写层,创建出另外一个 container。

那总不能每次都是一个新的环境,而无法保留我们之前做过的事对吧,因此 Docker 提供了一个指令,让我们可以把我们的容器打包成一个新的 image: docker container commit CONTAINER_ID [Repository:[Tag]]

shell 复制代码
# 在本文的示例中,执行指令如下,记得把 container id 换成你自己的:
$ docker container commit 4c9bee8fef4b node:20-updated

当执行完这个指令后,来查看一下目前本机中有的 images:

多了一个 node:20-updated ,用这个 image 来启动一个 container 看看:

用这个新的 image 建构出来的 container 会有 AAA.txt 这个文件,这是因为我们用 node:20-updated 这个我们 commit 出来的新 image,会在原有的 node:20 上面再加上一层(layer),并且把我们刚刚对那个 container 做的操作给封装起来,变成一个新的 image,当我们用这个新的 image 来建构新的 container 时,就会在其上再加上一层可写层来让我们操作:

Docker 很聪明的是,虽然我们在电脑中目前有 node:20node:20-updated 两个不同的 image,不过 Docker 并不会重复存两个 node:20 ,也就是重复的部分 Docker 只会存一个,这样就大大的节省了空间,此外也可以理解为什么要做成只读了,只读能做到共用而不互相影响。(关于这部分,在后面更进阶的讨论中,再来聊实际上的操作,这边就先这样简单地认识一下。)

当然,原本我们从 Docker Hub 上拉下来的 node:20 也不会只有单独一层,所以当我们在 pull 这个 image 时,会看到的画面是像下面这样:

按照这个过程, node:20 至少是由 8 个 layer 构成的。还记得我们 pull node:18 的过程吗:

这里 de4cac68b616 显示的是 Already exists ,这是因为我们在拉 node:20 时,已经拉过了,因此在拉 node:18 时,已经存在的就不用再重拉一次,这也再次验证了在 Docker 中,layer 是共用的、不会多占空间的。

这边有个不太常用的指令,但在熟悉 Docker 时可以玩玩看:

shell 复制代码
$ docker image history node:20

我们也来查看一下 node:20-updated 这个 image 并且比较看看:

这边可以看到 node:20-updated 比起 node:20 会多了一个 78c69cf95de8 ,这个就是我们刚刚启动第一个 container 操作后创建出来的一层,而这之下,就跟 node:20 一模一样了。

最后,这个 commit 出来的新 image,你也可以把它推送(push)到 Docker Hub 或是其他的 image repository 上去分享给别人。不过,在实际工作中,我们很少直接用 commit 这个指令,通常都会通过 Dockerfile 来做新的 image,这个就留到下次讨论了。

其他 Docker 生命周期指令

延续刚刚的测试,我们在 node:20-updated 创建出来的 container 中执行 exit 退出这个 container,这时候如果执行 docker container ls 会发现,只剩下两个 container,而刚刚退出的那个 container 已经看不到了:

但如果加上 -a ,就可以查看所有的 container:

这是因为我们刚刚在启动 container 时,要它执行的命令是 /bin/bash (且 PID 为 1),而当我们下了 exit 指令时,是在退出这个 bash,也就是退出了 PID 为 1 的这个 process,既然主要的 process 已经停止执行了,这个 container 自然也就关闭了,我们之后再来讨论怎么让 container 持续执行。从这里也可以从 Status 这个栏位看到 container 已经退出多久。

这时候如果想要回到这个 container 里,可以通过 docker container start CONTAINER_ID 来回到这个 container 里:

docker container start 只是重新启动这个 container,执行后我们还是在本机中,如果要进入 container 中,可以通过 exec 来进入 container 中,基本上这个指令常用的参数,例如 -it ,跟 docker container run 的时候差不多,我们就先不讨论了。

docker container exec -it CONTAINER_ID /bin/bash

这时候我们一样再执行 exit,然后再下 docker container ls 来检查,却会发现这个 container 还是活着的、跟刚刚不一样,我们通过 docker container exec 再进去一次,这次在里面执行一下 ps aux

这时候我们可以看到有两个 /bin/bash 的 process 在运行,我们通过 docker container exec 所执行的 /bin/bash 其实是 PID 为 26 的这一个,所以当我们执行 exit 时,退出的是这一个 bash,而 PID 为 1 的这个 /bin/bash 仍在执行中,所以这个 container 会持续存活。

docker container exec 有一个用起来很像的指令 docker container attach CONTINER_ID

attach 这个指令一样会进入 container 中,用起来跟刚刚的 docker container exec -it CONTAINER_ID 效果很像,但如果进去后执行 ps aux 会发现只有一个 /bin/bash process。你应该可以猜到,如果这时候执行了 exit ,会退出的是 PID 为 1 的 /bin/bash process,进而退出这个 container。此时再通过 docker container ls 或是 docker container ls -a 来确认,会发现这个 container 已经关闭了。所以,虽然 attach 也能进入这个 container 中,但我自己很少用,以免一不小心把 container 给关掉...

移除 image 跟 container

前面有提到,如果电脑中存有太多的 image,会占硬盘空间,因此建议定期清除用不到的 image,那我们来试试看清除刚刚拉下来的那个 node:18 ,其指令为 docker image rm IMAGE_ID :

这边可以看到,移除的时候也是逐层移除,不过,我们刚刚 pull node:18 时,明明有 8 层,这边却只移除了 4 层,这样真的有移干净吗?

但通过 docker image ls 来查看时,的确又已经看不到 node:18 了:

还记得前面有提到 layer 是共用的吗?也有提到 node:20node:18 有共用一些 layer,而因为共用的那些 layer 在 node:20 还需要用到,所以在移除 node:18 时,当然就不会移掉。

我们来试着移除刚刚我们创建出来的那个 node:20-updated 看看:

移除失败,根据错误信息可以知道是因为这个 image 正在被 container 422f119c50f6 给使用着,通过 docker container ls -a 检查看看:

果然,有一个 id 为 422f119c50f6 的 container 是通过这个 image 来建构的,如果这个 container 还在启动状态,那就需要先通过 docker container stop CONTAINER_ID 关闭这个 container,如果已经是关闭状态,那就可以用 docker container rm CONTAINER_ID 来移除这个 container:

这时候就可以用 docker image rm IMAGE_ID 来移除 image:

结语

这次纪录了 docker 的基本组件与操作,主要讨论的是 docker container 的执行与一些基本的操作。也许在做过这些练习与测试之后,你心里会有一大堆疑问,有的话是很棒的事情,但如一开始所说的,我想先讲一些基本的操作,先会基本的运用,有点感觉后,然后再慢慢深入讨论。

下次仍旧会是基础篇,让我们来讨论怎么让 container 间可以彼此沟通,然后再来讨论我最喜欢的 Dockerfile ,这可是我个人认为 docker 可以如此成功的重要因素之一。

指令整理

这边把本文讨论过的指令都整理起来,方便大家复习与查找,另外也会列出旧版的指令对照,推荐使用新版的指令,虽然较长,但具有一致的结构,非常好学!

shell 复制代码
# 查看 docker 版本
$ docker --version
$ docker version

# 查看 docker 系统信息
$ docker info

# 从 node:20 image 启动一个 docker container 并开启输出入
$ docker container run -it node:20 /bin/bash
# 旧版指令
# docker run -it node:20 /bin/bash

# 查看目前正在运行中的 container
$ docker container ls
# 旧版指令
# docker ps

# 查看全部的 container,包括已经停止的。 (a -> all)
$ docker container ls -a
# 旧版指令
# docker ps -a

# 从 DockerHub 上拉下版本为 18 的 node image
$ docker image pull node:18
# 旧版指令
# docker pull node:18

# 查看目前环境中的 docker images
$ docker image ls
# 旧版指令
# docker images

# 用 container id 为 4c9bee8fef4b 的 container 
# 创建一个叫做 node:20-updated 的 image
$ docker container commit 4c9bee8fef4b node:20-updated
# 旧版指令
# docker commit 4c9bee8fef4b node:20-updated

# 查看 node:20 这个 image 的历史
$ docker image history node:20
# 旧版指令
# docker history node:20

# 停止 4c9bee8fef4b 这个 contaienr 
$ docker container stop 4c9bee8fef4b
# 旧版指令
# docker stop 4c9bee8fef4b

# 启动 4c9bee8fef4b 这个 container
$ docker container start 4c9bee8fef4b
# 旧版指令
# docker start 4c9bee8fef4b

# 在 4c9bee8fef4b 这个 container 中执行 /bin/bash 这个命令,并且开启输出入
# 因为是执行 /bin/bash 又开启了输出入,所以就像是「进入」了这个 container 中
$ docker container exec -it 4c9bee8fef4b /bin/bash
# 旧版指令
# docker exec -it 4c9bee8fef4b /bin/bash

# 移除 4c9bee8fef4b 这个 container
$ docker container rm 4c9bee8fef4b
# docker rm 4c9bee8fef4b

# 移除所有停止的 containers
$ docker container prune -f

# 移除 node:18 这个 image
# 移除 image 前要先移除用这个 image 启动的 containers
$ docker image rm node:18
# 旧版指令
# docker rmi node:18
相关推荐
2402_8575893619 分钟前
Spring Boot新闻推荐系统设计与实现
java·spring boot·后端
J老熊27 分钟前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构
Benaso30 分钟前
Rust 快速入门(一)
开发语言·后端·rust
sco528231 分钟前
SpringBoot 集成 Ehcache 实现本地缓存
java·spring boot·后端
原机小子1 小时前
在线教育的未来:SpringBoot技术实现
java·spring boot·后端
吾日三省吾码1 小时前
详解JVM类加载机制
后端
努力的布布1 小时前
SpringMVC源码-AbstractHandlerMethodMapping处理器映射器将@Controller修饰类方法存储到处理器映射器
java·后端·spring
PacosonSWJTU1 小时前
spring揭秘25-springmvc03-其他组件(文件上传+拦截器+处理器适配器+异常统一处理)
java·后端·springmvc
记得开心一点嘛2 小时前
在Java项目中如何使用Scala实现尾递归优化来解决爆栈问题
开发语言·后端·scala
黄俊懿2 小时前
【深入理解SpringCloud微服务】手写实现各种限流算法——固定时间窗、滑动时间窗、令牌桶算法、漏桶算法
java·后端·算法·spring cloud·微服务·架构