# Docker 学习笔记

# Why & What

软件开发最大的麻烦事之一,就是环境配置。用户计算机的环境都不相同,你怎么知道自家的软件,能在那些机器跑起来?

用户必须保证两件事:操作系统的设置,各种库和组件的安装。只有它们都正确,软件才能运行。举例来说,安装一个 Python 应用,计算机必须有 Python 引擎,还必须有各种依赖,可能还要配置环境变量。

如果某些老旧的模块与当前环境不兼容,那就麻烦了。开发者常常会说:"它在我的机器可以跑了"(It works on my machine),言下之意就是,其他机器很可能跑不了。

环境配置如此麻烦,换一台机器,就要重来一次,旷日费时。很多人想到,能不能从根本上解决问题,软件可以带环境安装?也就是说,安装的时候,把原始环境一模一样地复制过来。

于是,为了解决这个问题,先后出现了下面的解决方案

  • 虚拟机
  • Linux 容器
  • Docker

# 虚拟机

很多人想到,可以使用虚拟机来解决问题

虚拟机(virtual machine)就是带环境安装的一种解决方案。它可以在一种操作系统里面运行另一种操作系统,

但虚拟机的缺点非常明显

  • 资源占用多,虚拟机会独占一部分内存和硬盘空间。它运行的时候,其他程序就不能使用这些资源了。哪怕虚拟机里面的应用程序,真正使用的内存只有 1MB,虚拟机依然需要几百 MB 的内存才能运行。
  • 冗余步骤多,虚拟机是完整的操作系统,一些系统级别的操作步骤,往往无法跳过,比如用户登录。
  • 启动慢,启动操作系统需要多久,启动虚拟机就需要多久。可能要等几分钟,应用程序才能真正运行。

# Linux 容器

由于虚拟机存在这些缺点,Linux 发展出了另一种虚拟化技术:Linux 容器(Linux Containers,缩写为 LXC)。

Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离。或者说,在正常进程的外面套了一个保护层。对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。

容器有点像轻量级的虚拟机,能够提供虚拟化的环境,但是成本开销小得多。

Linux 容器的优点如下

  • 启动快,容器里面的应用,直接就是底层系统的一个进程,而不是虚拟机内部的进程。所以,启动容器相当于启动本机的一个进程,而不是启动一个操作系统,速度就快很多。
  • 资源占用少,容器只占用需要的资源,不占用那些没有用到的资源;虚拟机由于是完整的操作系统,不可避免要占用所有资源。
  • 体积小,容器只要包含用到的组件即可,而虚拟机是整个操作系统的打包,所以容器文件比虚拟机文件要小很多。

# Docker

Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前最流行的 Linux 容器解决方案。

Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。

总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。

# How

# 安装

首先,下载 Docker

桌面版:https://www.docker.com/products/docker-desktop

服务器版:https://docs.docker.com/engine/install/#server

如果报 WLS2 的错误,下载并安装 WLS2 即可

下载链接:https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi

img

安装完毕后即可打开

image-20220610132039718

并且可以在命令行调用 docker 命令

# docker 版本
docker -v
# 运行结果
PS F:\LearnSpaces\Docker> docker -v
Docker version 20.10.16, build aa7e414

# 使用 Image

**Docker 把应用程序及其依赖,打包在 image 文件里面。** 只有通过这个文件,才能生成 Docker 容器。image 文件可以看作是容器的模板Docker 根据 image 文件生成容器的实例。同一个 image 文件,可以生成多个同时运行的容器实例。

image 是二进制文件。实际开发中,一个 image 文件往往通过继承另一个 image 文件,加上一些个性化设置而生成。举例来说,你可以在 Ubuntuimage 基础上,往里面加入 Apache 服务器,形成你的 image

image 文件是通用的,一台机器的 image 文件拷贝到另一台机器,照样可以使用。一般来说,为了节省时间,我们应该尽量使用别人制作好的 image 文件,而不是自己制作。即使要定制,也应该基于别人的 image 文件进行加工,而不是从零开始制作。

为了方便共享,image 文件制作完成后,可以上传到网上的仓库。Docker 的官方仓库 Docker Hub 是最重要、最常用的 image 仓库。此外,出售自己制作的 image 文件也是可以的。

# 列出本机的所有 image 文件。
docker image ls
# 删除 image 文件
docker image rm [imageName]

运行结果

PS F:\LearnSpaces\Docker> docker image ls
REPOSITORY               TAG       IMAGE ID       CREATED        SIZE
koa-demo-app             latest    f8b3e2dfd9c5   14 hours ago   676MB
redis                    latest    53aa81e8adfa   12 days ago    117MB
mysql                    oracle    a6664c768aba   2 weeks ago    444MB
node                     12.22.6   d9bd070812db   8 months ago   918MB
hello-world              latest    feb5d9fea6a5   8 months ago   13.3kB
nobetasena/hello-world   0.0.1     feb5d9fea6a5   8 months ago   13.3kB

image-20220610132849827

# 下载 Image

# 将 image 文件从仓库抓取到本地。
# docker image pull 是抓取 image 文件的命令。
# library/hello-world 是 image 文件在仓库里面的位置,其中 library 是 image 文件所在的组,hello-world 是 image 文件的名字。
# 由于 Docker 官方提供的 image 文件,都放在 library 组里面,所以它的是默认组,可以省略。因此,上面的命令可以写成下面这样。
docker image pull library/hello-world

下载完成后,可以通过 docker image ls 或者 Destop GUI 看到这个 image

# 运行 Image

注意, docker container run 命令具有自动抓取 image 文件的功能。如果发现本地没有指定的 image 文件,就会从仓库自动抓取。因此,前面的 docker image pull 命令并不是必需的步骤。

# 以 image 文件为模板,生成一个正在运行的容器实例
docker container run hello-world
# 对于那些不会自动终止的容器,必须使用 docker container kill 命令手动终止
# containID 可以使用 docker container ls 命令查询
docker container kill [containID]

运行结果如下

# 输出这段提示以后,hello world 就会停止运行,容器自动终止。
PS F:\LearnSpaces\Docker> docker -v
Docker version 20.10.16, build aa7e414
PS F:\LearnSpaces\Docker> docker container run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.

# 发布 Image

容器运行成功后,就确认了 image 文件的有效性。这时,我们就可以考虑把 image 文件分享到网上,让其他人使用。

发布过程如下

第一步,去 hub.docker.comcloud.docker.com 注册一个账户,注册完毕后登录

image-20220610134132781

第二步,在 Docker Desktop 上登录

image-20220610134316974

第三步,为本地的 image 标注用户名和版本

# docker 命令
docker image tag [imageName] [username]/[repository]:[tag]
# 比如说
docker image tag hello-world nobetasena/hello-world-app:0.0.1

第四步,发布 image 文件。

# 调用 push 命令发布
docker image push [username]/[repository]:[tag]
# 比如说
docker image push nobetasena/hello-world-app:0.0.1

效果如下

image-20220610135030125

image-20220610135111625

# 使用 Container

image 文件生成的容器实例,本身也是一个文件,称为容器文件。

也就是说,一旦容器生成,就会同时存在两个文件: image 文件和容器文件。而且关闭容器并不会删除容器文件,只是容器停止运行而已。

image-20220610135718444

# 查询所有的 container

# 列出本机正在运行的容器
docker container ls
# 列出本机所有容器,包括终止运行的容器
docker container ls --all

运行结果如下

image-20220610140007420

# 运行 container

docker container run 命令是新建容器,每运行一次,就会新建一个容器。同样的命令运行两次,就会生成两个一模一样的容器文件。

如果希望重复使用容器,就要使用 docker container start 命令,它用来启动已经生成、已经停止运行的容器文件。

docker container run 用于创建容器并执行

# 运行 koa-demo(这个名字是 image 文件的名字,如果有标签,还需要提供标签,默认是 latest 标签)
docker container run koa-demo
# 运行停止后自动删除容器文件
docker container run --rm koa-demo
# 指定更多参数
# -p 参数:容器的 3000 端口映射到本机的 8000 端口。
# -it 参数:容器的 Shell 映射到当前的 Shell,然后你在本机窗口输入的命令,就会传入容器。
# /bin/bash:容器启动以后,内部第一个执行的命令。这里是启动 Bash,保证用户可以使用 Shell。
docker container run -p 8000:3000 -it koa-demo:0.0.1 /bin/bash

docker container start 用于重复使用容器

# 重新启动容器
docker container start [containerID]

# 停止 container

# 在本机的另一个终端窗口,查出容器的 ID
docker container ls
# 停止指定的容器运行
docker container kill [containerID]

当然,你也可以直接在 GUI 里停止 container

image-20220610140957998

或者,你可以使用 stop 来代替 kill

# 停止 container
docker container stop [containerID]

前面的 docker container kill 命令终止容器运行,相当于向容器里面的主进程发出 SIGKILL 信号。而 docker container stop 命令也是用来终止容器运行,相当于向容器里面的主进程发出 SIGTERM 信号,然后过一段时间再发出 SIGKILL 信号。

这两个信号的差别是,应用程序收到 SIGTERM 信号以后,可以自行进行收尾清理工作,但也可以不理会这个信号。如果收到 SIGKILL 信号,就会强行立即终止,那些正在进行中的操作会全部丢失。

# 进入命令行

docker container exec 命令用于进入一个正在运行的 docker 容器。如果 docker run 命令运行容器的时候,没有使用 -it 参数,就要用这个命令进入容器。一旦进入了容器,就可以在容器的 Shell 执行命令了。

docker container logs 命令用来查看 docker 容器的输出,即容器里面 Shell 的标准输出。如果 docker run 命令运行容器的时候,没有使用 -it 参数,就要用这个命令查看输出。

# 查看容器的输出
docker container logs [containerID]
# 进入正在运行的 docker 容器
docker container exec -it [containerID] /bin/bash

# Dockerfile 文件

Dockerfile 文件。它是一个文本文件,用来配置 image,而 Docker 根据 该文件生成二进制的 image 文件

Dockerfile 文档

简单来说,Dockerfile 用于制作自己的镜像文件 (image)

接下来,我们用一个项目来演示如何使用 Dockerfile

# 下载测试项目

示例项目代码:https://github.com/gzyunke/test-docker

这是一个 Nodejs + Koa2 写的 Web 项目,提供了简单的两个演示页面。
软件依赖:nodejs
项目依赖库:koa、log4js、koa-router

# 下载代码
git clone https://github.com/gzyunke/test-docker

image-20220610142430557

# 编写相关文件

首先,在项目的根目录下,新建一个文本文件 .dockerignore ,写入下面的内容

上面代码表示,这三个路径要排除,不要打包进入 image 文件。如果你没有路径要排除,这个文件可以不新建。

node_modules
Dockerfile
.git
.idea

然后,在项目的根目录下,新建一个文本文件 Dockerfile ,写入下面的内容

# 该 image 文件继承官方的 node image,冒号表示标签,这里标签是 11,即 11 版本的 node。
FROM node:11
# 复制代码
# 将当前目录下的所有文件(除了.dockerignore 排除的路径),都拷贝进入 image 文件的 /app 目录。
ADD . /app
# 指定接下来的工作路径为 /app
WORKDIR /app
# 在 /app 目录下,运行 npm install 命令安装依赖。注意,安装后所有的依赖,都将打包进入 image 文件
RUN npm install --registry=https://registry.npm.taobao.org
# 将容器 8080 端口暴露出来, 允许外部连接这个端口
EXPOSE 8080
# CMD 指令只能一个,是容器启动后执行的命令,算是程序的入口。
# 如果还需要运行其他命令可以用 && 连接,也可以写成一个 shell 脚本去执行。
# 例如 CMD cd /app && ./start.sh
CMD node app.js

# 创建 image 文件

有了 Dockerfile 文件以后,就可以使用 docker build 命令创建 image 文件了

# 创建 image
# test-docker-app 是要生成 image 名称
# 那个。是用来指定 Dockerfile 的文件路径的
docker image build -t test-docker-app .

运行成功的效果如下

image-20220610150833973

image-20220610150847134

image-20220610150952240

# 运行 Image 文件

docker container run 命令会从 image 文件生成容器。

命令参考文档:https://docs.docker.com/engine/reference/run/

# -p 参数:容器的 8080 端口映射到本机的 9000 端口。
# -d 参数:后台运行
# -it 参数:容器的 Shell 映射到当前的 Shell,然后你在本机窗口输入的命令,就会传入容器。
# test-docker-app:image 文件的名字(如果有标签,还需要提供标签,默认是 latest 标签)。
# test-docker-app-container:container 文件的名字
docker run -p 9000:8080 -it -d --name test-docker-app-container test-docker-app

运行效果如下

# 命令行输出
PS F:\LearnSpaces\Docker\test-docker> docker run -p 9000:8080 -it -d --name test-docker-app-container test-docker-app
ea8bb9b90376e82a8dc7411345f022d83697b5e02c0f86b42a6dbae9b4559563

浏览器效果

image-20220610152247398

image-20220610152408751

# Compose

Docker 是一个容器工具,提供虚拟环境。

站在 Docker 的角度,软件就是容器的组合:业务逻辑容器、数据库容器、储存容器、队列容器......Docker 使得软件可以拆分成若干个标准化容器,然后像搭积木一样组合起来。

# Why Compose

比如说,如果你要创建一个 Wordpress 应用,你需要为 Wordpressmysql 分别创建不同的 image 并关联他们

# 新建并启动 MySQL 容器
docker container run \
  -d \
  --rm \
  --name wordpressdb \
  --env MYSQL_ROOT_PASSWORD=123456 \
  --env MYSQL_DATABASE=wordpress \
  mysql:5.7
# 基于官方的 WordPress image,新建并启动 WordPress 容器
# 并在启动时设置和数据库的关联
docker container run \
  -d \
  --rm \
  --name wordpress \
  --env WORDPRESS_DB_PASSWORD=123456 \
  --link wordpressdb:mysql \
  wordpress

因此,Docker 提供了一种更简单的方法,来管理多个容器的联动(注意,我这里只有两个容器,所以可能不是很明显,真正的后台项目,可能需要五六个容器甚至更多,这时候一个个启动就非常麻烦了)。

# What Compose

Compose 是 Docker 公司推出的一个工具软件,可以管理多个 Docker 容器组成一个应用。你需要定义一个 YAML 格式的配置文件 docker-compose.yml ,写好多个容器之间的调用关系。然后,只要一个命令,就能同时启动 / 关闭这些容器。

# 启动所有服务
$ docker-compose up
# 关闭所有服务
$ docker-compose stop
# 删除容器文件
$ docker-compose rm

# How

# 安装

Mac 和 Windows 在安装 docker 的时候,会一起安装 docker compose。

可以用下面的命令测试

# 查看版本
docker-compose -v
# 使用

创建一个 docker-compose.yml 文件

version: '3.1'
services:
  wordpress:
    image: wordpress
    restart: always
    ports:
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb
    volumes:
      - wordpress:/var/www/html
  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - db:/var/lib/mysql
volumes:
  wordpress:
  db:

image-20220610202759885

输入 docker-compose up 启动容器

docker-compose up

效果如下
image-20220610202814110

image-20220610203229527

image-20220610202942261

# 外部存储

外部存储可以用于建立容器外部到容器内部的映射

适用于下面的场景

  • 想在外部文件里操作容器里的 mysqlnginx 的配置
  • 解决 Docker 运行后,我们改了项目代码不会立刻生效,需要重新 buildrun 的问题

# 分类

  • Volumes:存储在 Docker Host 文件系统的一个路径下,这个路径是由 Docker 来进行管理,路径默认是 /var/lib/docker/volumes/,非 Docker 的进程不能去修改这个路径下面的文件,所以说 Volumes 是持久存储数据最好的一种方式。
  • Bind mounts:可以存储在 Docker Host 文件系统的任何位置,它们甚至可能是重要的系统文件或目录,非 Docker 的进程或者 Docker 容器可能随时对其进行修改,存在潜在的安全风险。
  • Tmpfs:只存储在 Docker Host 的系统内存中,不会写入到系统的文件系统中,不会持久存储。

types of mounts and where they live on the Docker host

# 挂载方式:Bind

这里我们新建一个项目

# 创建 vue3 项目
npm init vite@latest vue3-app --template vue

注意 vite 要开启网络访问

//vite 需要开启网络访问
//vite.config.js 开启 host
export default defineConfig({
  plugins: [vue()],
  server: {
    host: '0.0.0.0',
  },
});

然后运行下面的命令

# -d: 以守护进程的方式让容器在后台运行,在这您之 前可能使用的是 pm2 来守护进程
# -it: -i 和 - t 的缩写 告诉 Docker 容器保持标准输入流对容器开放,即使容器没有终端连接
# --name: 将容器命名为 vue3app,这样访问和操作容 器等就不需要输入一大串的容器 ID
# --privileged: 让容器的用户在容器内能获取完全 root 权限
# -p: 映射端口
# node:12.22.6: 指定下载的 node 版本
# /bin/bash -c "cd /app/vue2 && node -v && npm install && npm run serve": /bin/bash:是在让容器分配的虚拟终端以 bash 模式执行命令
# -v F:\LearnSpaces\Docker\vue3-app:/app/vue:将主机的 vue3-app 目录 (命令行这里只能写绝对路径哈) 下的内容挂载到容器的目录 /app/vue 内,如果容器的指定目录有文件 / 文件夹,将被清空。挂载后,容器修改 /app/vue 目录的内容,也是在修改主机目录 F:\LearnSpaces\Docker\vue3-app 的内容
# 参数 --volume(或简写为 - v)只能创建 bind mount
# docker run -it -d --name vue3app --privileged -p 9000:3000 -v F:\LearnSpaces\Docker\vue3-app:/app/vue node:12.22.6 /bin/bash -c "cd /app/vue && node -v && npm install && npm run dev"
docker run \
-it \
-d  \
--name vue3app \
--privileged \
-p 9000:3000 \
-v F:\LearnSpaces\Docker\vue3-app:/app/vue \
node:12.22.6 \
/bin/bash -c "cd /app/vue && node -v && npm install && npm run dev"

此时项目的效果如下

image-20220610212441774

image-20220610212225385

# 外部修改映射到 container

这时我们删一点代码

image-20220610212319851

然后打开控制台,进入 container

# 进入控制台
docker container exec -it c873b62f8d78 /bin/bash
# 列出文件列表
ls
# 输出文件内容
cat ./app/vue/src/components/HelloWorld.vue

可以看到,文件内容已经被更改了

image-20220610213044519

这时我们重启一下容器(因为 vite 没观测到文件更新,我也不知道为什么),就可以看到页面更新了

image-20220610213306700

image-20220610213330135

# container 修改映射到外部

使用以下命令测试

# 创建 index.css
touch ./app/vue/index.css
# 编辑文件
cat >> ./app/vue/index.css

效果如下

image-20220610215111813

image-20220610215219228

# 挂载方式:Volume

待补

# 参考

docker 快速入门:https://docker.easydoc.net/doc/81170005/cCewZWoN/lTKfePfP

docker 入门教程:https://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html

Docker 入门教程:https://www.bilibili.com/video/BV11L411g7U1?spm_id_from=333.337.search-card.all.click&vd_source=f3db65161ddb2e9d95a3435e37b558b8

https://blog.51cto.com/u_15127549/3609056

Docker 与数据:三种挂载方式:https://juejin.cn/post/6993979788020940830

利用 docker 搭建前端开发环境:https://juejin.cn/post/6932808129189150734

给前端的 docker 10 分钟真・快速入门指南:https://juejin.cn/post/7050304120082661407

Docker 持久存储介绍:https://zhuanlan.zhihu.com/p/56009492

更新于 阅读次数