Docker的一个简单例子(二)

文章目录

  • 环境
  • 示例
    • 持久化数据
    • bind mount
    • 多容器应用
    • Docker Compose
  • 参考

环境

  • RHEL 9.3
  • Docker Community 24.0.7

示例

持久化数据

默认情况下,各个容器之间的文件系统是相互独立的。即使两个容器来自同一个image,对其中一个容器的修改,对另一个容器也是不可见的。

我们来实际操作一下。

首先启动一个容器,创建 /data.txt 文件,其内容是一个随机数:

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

该命令启动了一个 ubuntu image的容器,并运行了两条命令:

  • shuf -i 1-10000 -n 1 -o /data.txt :产生一个随机数,并写到 /data.txt 文件里
  • tail -f /dev/null :仅仅是为了运行一条“可持续运行”的命令,否则 docker run 命令就会立即结束

注: docker run 只能运行一条命令,本例为了运行两条命令,用 && 来连接,这是bash的语法。

查看容器:

➜  ~ docker ps
CONTAINER ID   IMAGE             COMMAND                  CREATED          STATUS          PORTS                      NAMES
1216910a048e   ubuntu            "bash -c 'shuf -i 1-…"   13 seconds ago   Up 12 seconds                              gracious_pike
......

我们来看一下该容器里的 /data.txt 文件:

➜  ~ docker exec 1216910a048e cat /data.txt 
9458

可见,文件已经创建好了。

接下来,我们再启动一个 ubuntu image的容器,并查看 /data.txt 文件:

➜  ~ docker run -it ubuntu ls /data.txt
ls: cannot access '/data.txt': No such file or directory

可见,在第二个容器里,没有 /data.txt 文件。

最后,把两个容器都删除。

接下来,我们来实现数据的持久化。

回到我们之前的 getting-started 应用。该应用会把数据存储在SQLite数据库里,其位置为 /etc/todos/todo.db

通过创建一个volume,并将其mount到容器里的 /etc/todos 目录,我们就可以实现数据持久化。

首先通过 docker volume create 命令创建一个volume:

docker volume create todo-db

删除之前运行的容器,然后运行一个新的容器:

docker run -dp 0.0.0.0:3000:3000 --mount type=volume,src=todo-db,target=/etc/todos getting-started

打开浏览器,访问 http://localhost:3000 ,添加一些item:

Docker的一个简单例子(二)_第1张图片

删除容器:

docker rm -f 

重新启动一个容器:

docker run -dp 0.0.0.0:3000:3000 --mount type=volume,src=todo-db,target=/etc/todos getting-started

打开浏览器,访问 http://localhost:3000 ,可以看到之前添加的item仍然还在。

那么问题来了,持久化的数据保存到哪里了呢?

看一下volume:

➜  ~ docker volume ls
DRIVER    VOLUME NAME
local     todo-db
......
➜  ~ docker volume inspect todo-db
[
    {
        "CreatedAt": "2024-01-01T10:00:38+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
        "Name": "todo-db",
        "Options": null,
        "Scope": "local"
    }
]

可见,mount point是 "/var/lib/docker/volumes/todo-db/_data" ,这就是宿主机上的路径。

在宿主机上查看该目录:

➜  ~ sudo ls -l "/var/lib/docker/volumes/todo-db/_data"
total 8
-rw-r--r--. 1 root root 8192 Jan  1 11:23 todo.db

同样,如果我们在宿主机上创建一个文件,比如 /var/lib/docker/volumes/todo-db/_data/test.txt ,然后在容器里也可以访问:

➜  ~ docker exec 885e3594b2e0 cat /etc/todos/test.txt
haha

bind mount

上一节使用了volume mount,这一节介绍bind mount。

使用bind mount,把宿主机上的指定目录mount到容器里。当宿主机上的文件有变化时,容器里的文件同样也立即变化。

volume mount和bind mount的对比:

Volume mount Bind mount
宿主机路径 Docker决定 个人决定
示例 type=volume,src=my-volume,target=/usr/local/data type=bind,src=/path/to/data,target=/usr/local/data
向volume注入容器内容 Yes No
是否支持volume驱动 Yes No

在宿主机的 getting-started-app 根目录下,运行:

docker run -it --mount type=bind,src="$(pwd)",target=/src ubuntu bash

--mount 的参数如下:

  • type=bind :指定mount类型
  • src="$(pwd)" :指定宿主机路径
  • target=/src :指定容器路径

现在,我们已经进入了容器,查看 /src 目录:

root@9c90854addd6:/# ls -l /src
total 156
-rw-r--r--. 1 1000 1000    209 Dec 31 11:17 Dockerfile
-rw-r--r--. 1 1000 1000    269 Dec 31 09:19 README.md
-rw-r--r--. 1 1000 1000    648 Dec 31 09:19 package.json
drwxr-xr-x. 4 1000 1000     39 Dec 31 09:19 spec
drwxr-xr-x. 5 1000 1000     69 Dec 31 09:19 src
-rw-r--r--. 1 1000 1000 147266 Dec 31 09:19 yarn.lock

在容器里创建文件:

echo hello > /src/test.txt

在宿主机上也能访问:

➜  getting-started-app git:(main)cat test.txt
hello

反之,在宿主机上的文件修改,也会立即反映在容器里。

通过bind mount,我们可以把源代码mount到容器里,在容器里build和运行应用。

在宿主机的 getting-started-app 根目录下,运行:

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"

可通过 docker logs 查看容器log。或者 docker logs -f ,持续更新log。

注意:在国内会非常慢,还经常失败,要多试几次。

➜  getting-started-app git:(main) ✗ docker logs -f 8fb9ce6b6953b4b9013db782eb483591a0df137f2f34c67174cb1cd04ac92250
yarn install v1.22.19
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
Done in 163.98s.
yarn run v1.22.19
$ nodemon -L src/index.js
[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node src/index.js`
Using sqlite database at /etc/todos/todo.db
Listening on port 3000

按“Ctrl + C” 退出。

打开浏览器,访问 http://localhost:3000 ,确保应用工作正常。

接下来,在宿主机上修改源代码,比如修改 src/static/js/app.js 文件,把按钮上的文字由 Add Item 改为 Add ,保存,然后刷新浏览器:

Docker的一个简单例子(二)_第2张图片

可见,在宿主机上修改源代码后,通过浏览器访问应用,会立即反映出来。

注:分两步走:

  1. 在宿主机修改源代码 -> 修改同步到容器
  2. nodemon 发现源代码有变化后,立即重启应用(这一步是自动的, nodemon 是在 package.json 里启动的)

调试完毕后,最后构建image:

docker build -t getting-started .

多容器应用

现在我们来将应用改为使用MySQL数据库。显然,基于“高内聚,低耦合”的原则,需要在另外一个容器里单独运行MySQL。也就是说,应用由两个容器组成。那么问题来了,容器之间如何通信?容器的运行都是相互隔离的,显然它们需要通过网络来互相通信。

有两种方式将容器与网络关联:

  • 在启动容器时,分配网络
  • 把已运行的容器连接到网络

首先创建网络:

docker network create todo-app

查看网络:

➜  ~ docker network ls
NETWORK ID     NAME       DRIVER    SCOPE
be48b324c533   todo-app   bridge    local
......

启动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:8.0

其中:

  • --network todo-app --network-alias mysql :设置要加入的网络,以及该容器在网络里的别名
  • -v todo-mysql-data:/var/lib/mysql :指定volume(注:之前没有创建过 todo-mysql-data volume,此时会自动被创建)
  • -e MYSQL_ROOT_PASSWORD=secret :设置环境变量
  • -e MYSQL_DATABASE=todos :设置环境变量

注: -v 选项比较复杂,既可以volume mount,也可以bind mount。本例中是mount一个volume。

下面是一些 -v 的源(即 : 左边)例子:

  • $(pwd) :正确,绝对路径
  • . :正确,宿主机相对路径
  • ./src :正确,宿主机相对路径
  • ./src/static :正确,宿主机相对路径
  • src :正确,表示一个volume名字
  • src/static :错误!Docker会把它当作volume的名字,但是包含了非法字符 /

接下来,我们先来测试一下MySQL数据库:

docker exec -it  mysql -u root -p

提示输入密码,输入 secret

登录成功后,查看数据库:

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| todos              |
+--------------------+
5 rows in set (0.00 sec)

可见,已经创建好 todos 数据库了。

最后,输入 exit 退出。

至此,MySQL数据库已经准备好了。

在把应用连接到MySQL之前,我们先来看一下 nicolaka/netshoot ,它包含了很多有用的网络调试工具。

启动 nicolaka/netshoot 容器,并连接到 todo-app 网络:

➜  ~ docker run -it --network todo-app nicolaka/netshoot
Unable to find image 'nicolaka/netshoot:latest' locally
latest: Pulling from nicolaka/netshoot
8a49fdb3b6a5: Pull complete 
f08cc7654b42: Pull complete 
bacdb080ad6d: Pull complete 
df75a2676b1d: Pull complete 
d30ac41fb6a9: Pull complete 
3f3eebe79603: Pull complete 
086410b5650d: Pull complete 
4f4fb700ef54: Pull complete 
5a7fe97d184f: Pull complete 
a6d1b2d7a50e: Pull complete 
599ae1c27c63: Pull complete 
dd5e50b27eb9: Pull complete 
2681a5bf3176: Pull complete 
2517e0a2f862: Pull complete 
7b5061a1528d: Pull complete 
Digest: sha256:a7c92e1a2fb9287576a16e107166fee7f9925e15d2c1a683dbb1f4370ba9bfe8
Status: Downloaded newer image for nicolaka/netshoot:latest
                    dP            dP                           dP   
                    88            88                           88   
88d888b. .d8888b. d8888P .d8888b. 88d888b. .d8888b. .d8888b. d8888P 
88'  `88 88ooood8   88   Y8ooooo. 88'  `88 88'  `88 88'  `88   88   
88    88 88.  ...   88         88 88    88 88.  .88 88.  .88   88   
dP    dP `88888P'   dP   `88888P' dP    dP `88888P' `88888P'   dP   
                                                                    
Welcome to Netshoot! (github.com/nicolaka/netshoot)
Version: 0.11

输入 dig mysql ,查看 mysql 的网络信息:


 1ae405a48c1c  ~  dig mysql

; <<>> DiG 9.18.13 <<>> mysql
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 61648
;; 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: 8 msec
;; SERVER: 127.0.0.11#53(127.0.0.11) (UDP)
;; WHEN: Mon Jan 01 10:17:08 UTC 2024
;; MSG SIZE  rcvd: 44

可见, mysql 的IP地址是 172.18.0.2 。尽管 mysql 不是一个有效的hostname,Docker可以将其解析为IP地址,因为我们之前给MySQL容器设置过 --network-alias 。也就是说,我们只需把应用连接到 mysql ,就能连接MySQL数据库了。

在应用端需要设置一些MySQL连接的环境变量:

  • MYSQL_HOST
  • MYSQL_USER
  • MYSQL_PASSWORD
  • MYSQL_DB

注:在源代码里判断这些环境变量并做相应处理,Docker只是传入环境变量。

注意:在开发环境里,通过设置环境变量来设置MySQL连接信息的做法是OK的,但在生产环境里不推荐这么做,更安全的做法是,把这些私密信息放在文件里,然后mount到容器。很多应用也支持带 _FILE 后缀的环境变量,指定包含这些变量的文件。

接下来,启动应用,在 getting-started-app 根目录下,运行:

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"

其中:

  • -v "$(pwd):/app" :bind mount宿主机当前目录
  • --network todo-app :指定了要加入的网络,本例没有指定别名,因为不需要别人连接它
  • -e MYSQL_HOST=mysql :和下面几个环境变量一起,指定了数据库的连接信息

注: -v 选项比较复杂,既可以volume mount,也可以bind mount。本例中是bind mount一个宿主机目录(最好用绝对路径,如果用相对路径,貌似必须从 . 开始)。

然后,通过 docker ps 查看容器ID,并查看其log:

docker logs -f 

如下:

➜  getting-started-app git:(main) ✗ docker logs -f edf21888a43a
yarn install v1.22.19
[1/4] Resolving packages...
success Already up-to-date.
Done in 0.35s.
yarn run v1.22.19
$ nodemon -L src/index.js
[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node src/index.js`
Waiting for mysql:3306.
Connected!
Connected to mysql db at host mysql
Listening on port 3000

可见,已经连接上mysql数据库。

打开浏览器,访问 http://localhost:3000 ,添加一些item。

接下来,在MySQL容器里,查看数据:

mysql> use todos;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from todo_items;
+--------------------------------------+-------+-----------+
| id                                   | name  | completed |
+--------------------------------------+-------+-----------+
| 75900696-2daa-4e0e-91ef-07b8c4f532f0 | item1 |         0 |
| c4c9c814-22c4-4604-b145-c2380184e1b1 | item2 |         0 |
+--------------------------------------+-------+-----------+
2 rows in set (0.00 sec)

可见,刚刚添加的 itemitem2 已经保存在数据库里了。

Docker Compose

Compose用来定义多容器应用。只需在一个YAML文件里定义各个service,然后只用一条命令就能启动所有东西(容器,网络,volume等)。

getting-started-app 根目录下,创建 compose.yaml 文件如下:

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

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

volumes:
  todo-mysql-data:

在该YAML文件中,定义了两个service,表示将会启动两个容器。

这两个容器会自动加到同一个网络里。service名字就是其网络别名:

  • app
  • mysql

我们可以把这两个service和前面对应的 docker run 命令对比一下,内容基本是一致的,只是语法不同而已。

注意:前面提到, docker run 命令里,若指定的volume不存在,则Docker会自动创建该volume。但对于Compose,必须显式创建volume。本例中,显式创建了volume todo-mysql-data

删除之前运行的容器,然后运行:

➜  getting-started-app git:(main) ✗ docker compose up -d
[+] Running 4/4
 ✔ Network getting-started-app_default           Created                                                                                                                                                                                0.2s 
 ✔ Volume "getting-started-app_todo-mysql-data"  Created                                                                                                                                                                                0.0s 
 ✔ Container getting-started-app-mysql-1         Started                                                                                                                                                                                0.0s 
 ✔ Container getting-started-app-app-1           Started 

可见,创建了一个网络,创建了一个volume,并且启动了两个容器。

查看compose的log:

➜  getting-started-app git:(main) ✗ docker compose logs -f 
......
app-1    | Connected to mysql db at host mysql
app-1    | Listening on port 3000
......
mysql-1  | 2024-01-01T14:11:24.084722Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.35'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.

可见,应用已经启动成功。按下“Ctrl + C”退出log。

打开浏览器,访问 http://localhost:3000 ,测试应用工作正常。

注:之前填过的item都不见了,这是因为和之前使用的不是同一个volume。之前用的是 todo-mysql-data ,现在用的是 getting-started-app_todo-mysql-data

查看网络:

➜  getting-started-app git:(main) ✗ docker network ls        
NETWORK ID     NAME                          DRIVER    SCOPE
1903ef27abaa   getting-started-app_default   bridge    local
......
➜  getting-started-app git:(main) ✗ docker network inspect 1903ef27abaa
......
        "Containers": {
            "2faf22f53ea7ee2bafbf5b817b27b8220f86af9a06ab7dad500bc7721bfac76a": {
                "Name": "getting-started-app-app-1",
                "EndpointID": "46bf8de5bb06982ae6ad121631d35b3fc9e20ed423978fdf0ecd7e29ede3ccaa",
                "MacAddress": "02:42:ac:16:00:02",
                "IPv4Address": "172.22.0.2/16",
                "IPv6Address": ""
            },
            "4d549752261d29c672484b9e6cb68d6746a32d8fe77fdd6459c5dde2e2da28e5": {
                "Name": "getting-started-app-mysql-1",
                "EndpointID": "d887f42ab3ed22f759e3f4bff45d83c6d2a162065a4237dd064f6c79585b7aa0",
                "MacAddress": "02:42:ac:16:00:03",
                "IPv4Address": "172.22.0.3/16",
                "IPv6Address": ""
            }
......

最后,删除所有东西:

➜  getting-started-app git:(main) ✗ docker compose down
[+] Running 3/3
 ✔ Container getting-started-app-app-1    Removed                                                                                                                                                                                       0.2s 
 ✔ Container getting-started-app-mysql-1  Removed                                                                                                                                                                                       3.2s 
 ✔ Network getting-started-app_default    Removed

注意:网络 getting-started-app_default 会和容器一起删除,但是volume getting-started-app_todo-mysql-data 不会删除。若想要删除volume,则需加上 --volumes 选项:

➜  getting-started-app git:(main) ✗ docker compose down --volumes                
[+] Running 4/4
 ✔ Container getting-started-app-mysql-1       Removed                                                                                                                                                                                  3.0s 
 ✔ Container getting-started-app-app-1         Removed                                                                                                                                                                                  0.2s 
 ✔ Volume getting-started-app_todo-mysql-data  Removed                                                                                                                                                                                  0.0s 
 ✔ Network getting-started-app_default         Removed

但是要慎用,一旦删除volume,持久化的数据就没了。下次再启动容器,之前的填过的item就不见了。

参考

  • https://docs.docker.com/get-started/
  • https://www.linuxidc.com/Linux/2015-09/123519.htm

你可能感兴趣的:(docker,docker)