Docker 整体介绍
Docker 是一种使用 Go 语言开发的容器工具。所谓容器,实际上是一种虚拟化技术,用于为应用提供虚拟化的运行环境,相较于虚拟机具有轻量级、低延迟的特性。
下面是对上述介绍的说明:
应用程序运行需要一定的依赖支持,即所谓运行环境.在软件的开发、测试和发布等过程中,必然面临环境迁移问题————需要多次应对环境变化问题。重复构建环境既不经济、也不高效、甚至可能对开发团队心理健康造成负面影响。
通常的解决方案是“将环境打包”,即将软件和运行环境一起交付给用户(下一阶段开发人员)。虚拟机毕竟是整个系统镜像,除了依赖还由过多不必要的东西,而且虚拟机这种在 Host OS 上运行 Guest OS 的形式相对笨重。容器技术则在这两方面表现更优。
总而言之,借助 Docker,可以像管理应用程序一样管理硬件,而且相对于虚拟机占用的资源少得多。
With Docker, you can manage your infrastructure in the same ways you manage your applications. By taking advantage of Docker’s methodologies for shipping, testing, and deploying code, you can significantly reduce the delay between writing code and running it in production.
Docker 架构介绍
类似于调试工具 GDB,Docker 也采用 Client-Server 架构,即用户使用 Docker Client 通过 SOKCET 等网络接口与作为 Server 的 Docker Daemon 交互。Docker Daemon 既可以部署在本地,也可以部署在远程服务器上,也就是说下图中的 Host 既可以是 127.0.0.1,也可以是某个服务器(使用远端的开发环境)。
下面将阐释图中的 Images、Containers 和 Registry 等概念的内涵。
Docker Daemon 和 Docker Client
Dokcer 的 Server 端,负责实际响应 Client 的请求,完成 镜像(Image)、容器(Container)等对象的管理。
Doker Daemon 支持与其他 Docker Daemon 通信。
Docker Registry
用于存储 Docker Image。Docker 还提供类似于镜像商店的平台 Docker Hub,公开发布 Docker Image。Docker 支持自建 Registry,使用类似于 Git,通过PUSH 和 PULL 命令发布和获取 Registry 中的 Docker Image。
Image 和 Continer
所谓 Docker 镜像,即打包好的环境。Docker Image 是只读的,Docker Daemon 可以根据 Docker Image 构建出 Docker Container。
Image 和 Continer 的关系,可以描述为
A container is a runnable instance of an image.
即 Container 是可运行的实例化 Image。说起来有点绕口,借用面向对象的概念,可以把二者的关系理解成:Class 和 Object 的关系。Image 有点像设计图,Docker 可用一个 Image 构建多个 Container 实例,即多个一模一样的、相互独立的环境。
实操1:镜像制作与容器化
准备工作
- 安装 Docker Desktop:https://docs.docker.com/get-docker/
- 安装 Git Client:https://git-scm.com/downloads
- 安装 VS Code https://code.visualstudio.com/
其实就是安装 Docker、Git 和 选择合适的编辑器。Git 和 文本编辑器大概都已经安装了,这里提一下 Docker 的安装。
建议使用官方脚本而不是按照上面链接的 Docker Desktop 安装教程进行安装。
只需要两个命令————用 Curl 获取安装脚本 test-docker.sh 和运行脚本:
curl -fsSL https://test.docker.com -o test-docker.sh
sudo sh test-docker.sh
安装过程输出如下:
获取实例源码
Docker 提供了学习用的软件源码,可以下载下来用于后续的实验。获取命令如下:
git clone https://github.com/docker/getting-started-app.git
源码目录结构如下:
├── getting-started-app/
│ ├── package.json
│ ├── README.md
│ ├── spec/
│ ├── src/
│ └── yarn.lock
制作镜像(Image)
-
进入源码目录
getting-started-app
-
创建 Docker 镜像制作脚本 Dockerfile(功能类似于 Makefile,说明制作镜像的参数)。Dockerfile 的内容改成:
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
EXPOSE 3000
大概意思是基于 node:18-alpine
这个镜像构建。
- 开始构建,用
-t
指定镜像的名字,最后是 Dockerfile 所在目录的相对路径
$> docker build -t getting-started .
可能出现如下问题:
这是因为没有安装 cmdtests,可以通过下面的命令安装:
$> sudo apt-get insall cmdtest
- 运行镜像,构建成运行应用的容器:
$> sudo docker run -dp 127.0.0.1:3000:3000 getting-started
上述命令表示运行镜像 getting-started
, 将容器的端口 3000 映射到主机的 127.0.0.1 地址的 3000 端口上。其中 -d
参数表示在后台运行容器,-p
参数用于指定端口映射。run
的镜像名要和前面 build
的 -t
指定的一致。
- 验证容器构建结果。
getting-started-app
是一个使用js
编写的前端工程,功能是在页面上创建和删除信息栏。
完成第 4 步后等十几秒钟,通过浏览器访问地址 ‘127.0.0.1:3000’, 可以看到如下页面:
容器间数据共享
容器内程序对文件的创建/更新都是在容器自有的文件系统镜像上进行的,单个容器运行时的文件操作对其它容器不可见。要实现文件操作的一致性,则需要借助 Volume
。
Volume
可以把容器某个文件路径挂载(mount)到主机(host)上,使容器内的更改也能在主机上看到。如果在容器重启的时候挂载同一个目录,那就能实现容器之间的文件共享。
更具象地说,现在这个应用程序 getting-started-app
把所有数据都存在同一个文件(一个 SQLite 数据库文件)里。只要在主机上保存这个文件,就能够共享给其它容器。
综上所述,我们需要一个 Volume
,用于把容器中的应用程序的数据保存在主机上,以便共享给其他容器。
创建 Volume。
使用 volaume create 命令创建一个 Volume
$> docker volume create todo-db
移除所有 getting-started-app
容器
-
移除所有
getting-started-app
容器。上图只有一个,CONTINER ID 是 7b53603df907, 则使用命令sudo docker rm -f 7b53603df907
移除这个容器。
启动镜像同时挂载 Volume
$> docker run -dp 127.0.0.1:3000:3000 --mount type=volume,src=todo-db,target=/etc/todos getting-started
和前面相比,这里多了一个 --mount
参数,type 是 volume
, src 要和前面使用 volaume create 命令创建的 Volume
保持一致。
测试挂载 Volume 的效果
-
打开页面
127.0.0.1:3000
,输入框中输入任意字符后点击Add Item
,在页面上创建新的 Item,然后移除getting-started-app
这个容器。 -
再次以挂载 Volume 的方式启动容器(注意要挂载同一个 Volume)。刷新刚才打开的页面,发现和移除容器前一致(刚才创建的 Item 没有因为重新启动容器而消失)。
Docker 容器间实时数据共享(bind mounts)
虽然 Volume
能够实现数据共享,但对容器中的应用和主机而言,这种共享更像是容器有一个 Write Back 策略的缓存,主机并不能实时收到应用对共享数据的修改,主机上对应用程序源码的修改也不能实时传递给容器。
Docker 提供的 bind mounts
方法用来解决上述问题。 通过 bind mount
把源码文挂载到容器上,每次保存文件都会立即把修改传递给容器。这样我们就能一边运行着容器调试源码了。
use a bind mount to mount source code into the container. The container sees the changes you make to the code immediately, as soon as you save a file.
当然,有人认为把开发工具放到容器里也行,但那样容器就过于臃肿了,失去了原本的轻薄特质。
bind mounts 命令格式
$> docker run -dp 127.0.0.1:3000:3000 \
-w /app --mount type=bind,src="$(pwd)",target=/app \
node:18-alpine \
sh -c "yarn install && yarn run dev"
- -w 指定命令的当前路径/工作路径
- –mount type=bind,src="
(
p
w
d
)
"
,
t
a
r
g
e
t
=
/
a
p
p
:把
‘
(pwd)",target=/app:把 `
(pwd)",target=/app:把‘PWD
挂载到容器的
/app` 目录下。注意这个路径必须是绝对路径形式,不然会遇到如下报错:
用下列命令把 /home/hhy/ws/getting-stared-app
挂载到容器的 /app
目录下:
$> sudo docker run -dp 127.0.0.1:3000:3000 \
-w /app --mount type=bind,src="/home/hhy/ws/getting-stared-app",target=/app \
node:18-alpine \
sh -c "yarn install && yarn run dev"
然后用下列命令查看 Docker 的 log(先用 docker ps
命令查看容器 ID, 这里演示的容器 ID 是 0x0a9c4c9d22 ):
$> sudo docker -f 0a9c4c9d22
上述命令运行结果如下:
测试 bind mounts 效果
- 浏览器打开页面
127.0.0.1:3000
- 修改主机上当前目录下的
src/static/js/app.js
,把 109 行的Add Item
修改成Item
,这对应页面上按钮的提示文字。 - 保存修改后刷新页面,发现按钮上的提示文字变成
Item
, 同步成功。
容器间通信
创建通信网络
容器间的通信时借助 Network
实现的,这也是一种 Docker 对象。要使用 Network
,首先需要创建一个对象:
$> docker network create todo-app
上述命令创建了一个名为 todo-app 的 Network
。
启动数据库(单独一个容器,连接到通信网络)
然后启动一个 Mysql 的容器 mysql:8.0
:
$> docker run -d \
--network todo-app --network-alias mysql \
-v todo-mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=todos \
mysql:8.0
本地没有这个镜像,Docker 会自动从 Registry 下载,启动过程如下:
运行 getting-started-app
使用如下命令启动我们前面多次用到的运行 getting-started-app 容器,将其连接到和数据库容器共享的 Network
todos。
$> docker run -dp 127.0.0.1:3000:3000 \
-w /app -v "$(pwd):/app" \
--network todo-app \
-e MYSQL_HOST=mysql \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=secret \
-e MYSQL_DB=todos \
node:18-alpine \
sh -c "yarn install && yarn run dev"
使用 sudo docker ps
查看容器列表,可以看到两个容器,一个是运行 getting-startted-app 的容器,另一个是数据库容器。
创建新数据(修改页面)
浏览器打开 http://127.0.0.1:3000
, 添加几个信息栏,如下:
验证数据库
使用如下命令打开 mysql 数据库:
$> docker exec -it <mysql-container-id> mysql -p todos
然后用命令 select * from todos_items
在数据库中搜索 todos_items
.
上述两条命令运行结果如下(注意 Enter password
后面输入启动数据库容器时的设置 -e MYSQL_ROOT_PASSWORD=secret
):