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
相关推荐
NiNg_1_2342 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk3 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*3 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue3 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man3 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
南猿北者5 小时前
docker容器
docker·容器
二十雨辰5 小时前
[linux]docker基础
linux·运维·docker
customer085 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源