默认情况下,各个容器之间的文件系统是相互独立的。即使两个容器来自同一个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 rm -f <container ID>
重新启动一个容器:
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
上一节使用了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 <container ID>
查看容器log。或者 docker logs -f <container ID>
,持续更新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
,保存,然后刷新浏览器:
可见,在宿主机上修改源代码后,通过浏览器访问应用,会立即反映出来。
注:分两步走:
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 <container ID> 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 <container ID>
如下:
? 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)
可见,刚刚添加的 item
和 item2
已经保存在数据库里了。
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