# Docker实践

# 开始使用

# 开启docker

执行 docker run -dp 80:80 docker/getting-started 运行一个容器

# Docker 面板

可视化Docker管理,打开该面板即可看到我们刚启动的容器

# 什么是容器

容器只是计算机上的另一个进程,已与主机上的所有其他进程隔离。这种隔离利用了Linux上已有很长时间的内核名称空间和cgroup。

# 什么是容器镜像

运行容器时,它使用隔离的文件系统。此自定义文件系统由容器映像提供。由于映像包含容器的文件系统, 因此它必须包含运行应用程序所需的所有内容-所有依赖项,配置,脚本,二进制文件等。 该映像还包含容器的其他配置,例如环境变量,要运行的默认命令和其他元数据。

# 我们的应用

# 获取 App源码

点击下载 (opens new window),下载完成后解压并打开

# 构建APP 容器镜像

在上一步获取的源码的app目录里,新建 Dockerfile文件,输入下面内容

FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
1
2
3
4
5

切换到app目录下 ,输入下面命令开始构建镜像

docker build -t getting-started .

-t 给镜像打上标签,. 符号,表示去当前目录找Dockerfile

# 开启一个App容器

执行下面命名开启一个容器,用我们刚刚打的镜像getting-started

docker run -dp 3000:3000 getting-started

  1. -d, 在后台运行 detached
  2. -p 3000:3000 在主机的端口3000到容器的端口3000之间创建映射。

当控制台成功输出容器id的时候,打开浏览器http://localhost:3000/,就可以看到我们的应用。

# 更新容器

需要我们的应用在没有数据的时候提示 You have no todo items yet! Add one above!

# 更新代码

  1. src/static/js/app.js 的56行 改为 <p className="text-center">No items yet! Add one above!</p>
  2. 重新构建镜像,docker build -t getting-started .
  3. 启动新容器,docker run -dp 3000:3000 getting-started,此时发现报错
docker: Error response from daemon: driver failed programming external connectivity on endpoint heuristic_greider (cea4c33d11e0f8f1ca148c145dd24b986fcfdaacf9e03957e58072522ee4bfbb): Bind for 0.0.0.0:3000 failed: port is already allocated.
1

# 替换我们的旧容器

移除一个容器,需要先把他停止,一旦容器被停止了,他就可以被移除了。有两种移除方式:

  1. 使用Docker CLI 移除
  • docker ps 获取容器id
  • docker stop <the-container-id> 停止容器
  • docker rm <the-container-id> 移除容器
  • docker rm -f <the-container-id> 不需要停止,强制删除
  1. 使用Docker可视化工具删除

# 新建容器

删除我们的旧容器后,执行 docker run -dp 3000:3000 getting-started新建容器,刷新 http://localhost:3000/

# 分享容器

要共享容器,需要使用Docker registry,默认使用的是 Docker Hub (opens new window)

# 创建一个仓库

  1. 登录 Docker Hub
  2. 点击Create Repository 按钮
  3. 输入getting-started-xxx,比如我输入了 getting-started-czp
  4. 点击Create

# 推送镜像到远程仓库

没有tag

  1. docker login -u YOUR-USER-NAME. 登录Docker Hub
  2. docker tag getting-started YOUR-USER-NAME/getting-started-czp. 【docker tag 本地镜像源 远程镜像仓库】。 远程没有的时候会自动创建
  3. docker push YOUR-USER-NAME/getting-started-czp

需要tag

  1. docker login -u YOUR-USER-NAME. 登录Docker Hub
  2. docker tag getting-started YOUR-USER-NAME/getting-started-czp:test.
  3. docker push YOUR-USER-NAME/getting-started-czp:tset

# 测试我们的镜像

  1. 点击打开测试地址 (opens new window)
  2. 使用 Docker Hub 账户登录
  3. 点击 ADD NEW INSTANCE
  4. docker run -dp 3000:3000 YOUR-USER-NAME/getting-started-czp
  5. 点击 3000端口号,就会打开刚启动的Docker服务。

# 数据持久化

# 容器的文件系统

当容器运行的时候,它使用镜像中的各个层作为其文件系统。每个容器还拥有自己的“暂存空间”以创建/更新/删除文件。 即使使用相同的镜像,也不会在其他容器中看到任何更改。

# 简单案例

  1. 执行 docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"

shuf -i 1-10000 -n 1 -o /data.txt 输出一个 1-10000的随机数到 data.txt

tail -f /dev/null 这个命令是持续输出,保持容器运行

  1. 在 Docker 可视化工具中打开刚刚启动的ubuntu。执行cat /data.txt, 可以看到data.txt的数据

或者直接执行 docker exec <container-id> cat /data.txt,也可以看到数据

  1. 我们开启一个新的容器 docker run -it ubuntu ls /并查看文件系统,此时未发现data.txt

  2. 移除第一个ubuntu 容器 ,docker rm -f 32031d768702

# 容器卷 Container Volumes

  1. 创建容器卷docker volume create todo-db
  2. 移除存在的todo-list容器 docker rm -f <id>
  3. 开启新容器,并加入-v 参数, docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started
  4. 打开http://localhost:3000/新增几个todo-item
  5. 使用 docker ps获取所有容器id,使用 docker rm -f <id>删除容器
  6. 使用第3条命令再次执行

# 深入探讨

使用docker volume inspect todo-db 查看Docker实际上将数据存储在哪里

[
    {
        "CreatedAt": "2021-02-02T08:28:44Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/todo-db/_data", 磁盘上存储数据的实际位置
        "Name": "todo-db",
        "Options": {},
        "Scope": "local"
    }
]

1
2
3
4
5
6
7
8
9
10
11
12

# 使用绑定安装

使用绑定安装,我们可以控制主机上的确切安装点。 我们可以使用它来保留数据,但通常用于向容器中提供其他数据。 在处理应用程序时,我们可以使用绑定挂载将源代码挂载到容器中,以使其查看代码更改,响应并立即查看更改。

# 快速卷类型比较

绑定安装和命名卷是Docker引擎随附的两种主要卷类型。但是,可以使用其他卷驱动程序来支持其他用例(SFTP,Ceph,NetApp,S3等)

# 开始一个开发环境下的容器

  1. 把源码挂载在容器里
  2. 安装所有依赖,包括开发依赖
  3. 启动nodemon 检测文件改动

开始配置环境

  1. 执行下列代码
docker run -dp 3000:3000 \
    -w /app -v "$(pwd):/app" \
    node:12-alpine \
    sh -c "yarn install && yarn run dev"
1
2
3
4
  • -dp 3000:3000 在后台模式下运行并创建端口映射
  • -w /app 设置命令的“工作目录”或当前目录
  • -v "$(pwd):/app" 从容器中的主机将当前目录绑定挂载到/ app目录中
  • node:12-alpine 和我们Dockerfile 配置的 一样
  • sh -c "yarn install && yarn run dev" 安装开发依赖并启动服务
  1. 查看docker输出日志 docker logs -f --tail=10 <container-id>
  2. 修改 src/static/js/app.js文件的 {submitting ? 'Adding...' : 'Add Item'}{submitting ? 'Adding...' : 'Add'}
  3. 刷新浏览器可以看到按钮变了。
  4. 重新构建镜像 docker build -t getting-started .

# 多容器运行

当我们使用node + mongodb开发项目的时候,需要两个容器来跑应用。理由如下

  1. 数据隔离
  2. 单独的容器可让您隔离版本和更新版本
  3. 有可能本地开发的时候需要本地版本,服务器开发的时候需要服务器版本。
  4. 运行多个进程将需要一个进程管理器(容器仅启动一个进程),这增加了容器启动/关闭的复杂性

# 容器网络

容器间通过网络进行通信,如果两个容器在同一个网络,那么他们能通信,否则不可以。

# 开启MySQL

  1. 创建一个网络 docker network create todo-app
  2. 启动一个MySQL容器并将其附加到网络
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:5.7
1
2
3
4
5
6

我们通过-v 连接了卷,但是我们没有通过 docker volume create todo-mysql-data创建卷。实际上,当我们运行命令的时候 docker发现我们没有todo-mysql-data,他会给我们创建一个,并挂载。

  1. 确认下我们的数据库是否运行 docker exec -it <mysql-container-id> mysql -p
  • 输入密码 secret
  • 登录后输入 SHOW DATABASES;

# 连接到MySQL

使用 nicolaka/netshoot (opens new window) 容器,这个容器有很多有用的网络工具

  1. 使用nicolaka/netshoot镜像开启一个新的容器,确保它连接在同一个网络 docker run -it --network todo-app nicolaka/netshoot
  2. 输入dig mysql ,输出

[1] 🐳  → dig mysql

; <<>> DiG 9.14.12 <<>> mysql
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3399
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;mysql.				IN	A

;; ANSWER SECTION:
mysql.			600	IN	A	172.18.0.2

;; Query time: 2 msec
;; SERVER: 127.0.0.11#53(127.0.0.11)
;; WHEN: Tue Feb 02 10:27:24 UTC 2021
;; MSG SIZE  rcvd: 44

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

ANSWER SECTION: 下的172.18.0.2就是我们的容器通讯ip。

虽然mysql不是有效的主机名,但是Docker知道怎么通过主机名解析到IP。这意味着我们要连接到mysql容器,只需要知道别名就可以。

# 把应用的数据库连接到Mysql

  • MYSQL_HOST - the hostname for the running MySQL server
  • MYSQL_USER - the username to use for the connection
  • MYSQL_PASSWORD - the password to use for the connection
  • MYSQL_DB - the database to use once connected
  1. 执行下来操作
docker run -dp 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:12-alpine \
  sh -c "yarn install && yarn run dev"
1
2
3
4
5
6
7
8
9
  1. 查看应用日志 docker logs <container-id>,会看到 Connected to mysql db at host mysql

  2. 打开http://localhost:3000/,新增几个todo-item

  3. 查看数据库数据 docker exec -it <mysql-container-id> mysql -p todos

输入密码后,执行 select * from todo_items;可以看到打印的数据

# 容器组合

  1. 在app目录下新建 docker-compose.yml,输入下列命令
version: "3.7"

services:
  app:
    image: node:12-alpine
    command: sh -c "yarn install && yarn run dev"
    ports:
      - 3000:3000
    working_dir: /app
    volumes:
      - ./:/app
    environment:
      MYSQL_HOST: mysql
      MYSQL_USER: root
      MYSQL_PASSWORD: secret
      MYSQL_DB: todos

  mysql:
    image: mysql:5.7
    volumes:
      - todo-mysql-data:/var/lib/mysql
    environment: 
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: todos

volumes:
  todo-mysql-data:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  1. 删除我们前面启动的容器

  2. 执行 docker-compose up -d,控制台打印

Creating network "app_default" with the default driver
Creating volume "app_todo-mysql-data" with default driver
Creating app_app_1   ... done
Creating app_mysql_1 ... done
1
2
3
4
  1. 执行 docker-compose logs -f app可以查看日志

# Docker面板显示

Docker面板可以看见运行的app组,这个名字默认是docker-compose.yml所在目录的项目名,展开后可以看看见

两个子容器,名字是以 project-name>_<service-name>_<replica-number>组成的。

# 拆开组

在app目录下执行 docker-compose down ,所有容器将停止,网络被移除。 但是volumes不会被移除,如果需要移除则加上 --volumes

# 构建镜像的最佳实践

# 安全扫描 (opens new window)

当构建镜像的时候使用docker scan命令进行漏洞检测。

也可以在Docker Hub的配置中设置自动扫描。

# 镜像分层

  1. 可以使用 docker image history <image-name>查看镜像分层。使用此功能,您还可以快速查看每个分层的大小,从而帮助诊断大镜像。

  2. 使用上面命令的时候看见数据被折断了,可以使用 docker image history --no-trunc getting-started打印完整的输出。

# 分层缓存

图层更改后,所有下游图层也必须重新创建