入门教程
体系架构
docker 使用(客户端-服务端)的体系架构;其中,docker 守护进程(daemon)负责编译、运行以及分发容器(container)等重要工作,扮演着体系架构中服务端的角色;而 docker 客户端则通过 REST 接口与 docker 守护进程通信,将使用者的指令发送给守护进程,他们既可以处于同一系统内,也可以是远程连接;另外 Docker Compose 也属于客户端,它主要用于编排多个容器的场景。
基本使用
$ docker run -d -p 80:80 docker/getting-started
这是最基本的 docker 运行指令,其中的参数说明如下:
- -d - 在后台运行容器
- -p 80:80 - 将主机的 80 端口映射至容器的 80 端口
- docker/getting-started - 用于创建容器的镜像
上面例子中的参数可简写:
$ docker run -dp 80:80 docker/getting-started
示例应用
在余下的教程中,我们将使用一个示例来说明 docker 的使用,该示例是一个 Node.js 项目;即使你不熟悉 Node.js 也没有关系,这里并不涉及开发内容。该项目地址位于 Github,你可以直接下载 zip 文件。
下载解压后,用你喜欢的编辑器打开,可以看到文件格式如下:
$ ls -l
总用量 172
-rw-rw-r-- 1 wey wey 646 12月 14 11:11 package.json
drwxrwxr-x 4 wey wey 4096 12月 14 11:11 spec
drwxrwxr-x 5 wey wey 4096 12月 14 11:11 src
-rw-rw-r-- 1 wey wey 162208 12月 14 11:11 yarn.lock
编译镜像
首先需要创建一个 Dockerfile 文件,该文件包含了创建容器镜像所需的指令;在本例中,创建 Dockerfile 如下:
$ ls -l
总用量 172
-rw-rw-r-- 1 wey wey 0 12月 14 11:27 Dockerfile
-rw-rw-r-- 1 wey wey 646 12月 14 11:11 package.json
drwxrwxr-x 4 wey wey 4096 12月 14 11:11 spec
drwxrwxr-x 5 wey wey 4096 12月 14 11:11 src
-rw-rw-r-- 1 wey wey 162208 12月 14 11:11 yarn.lock
# syntax=docker/dockerfile:1
FROM node:12-alpine
RUN apk add --no-cache python3 g++ make
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
注意,Dockerfile 文件是没有扩展名 .txt 的。
然后执行 docker build 命令编译镜像:
$ docker build -t getting-started .
Sending build context to Docker daemon 4.641MB
Step 1/6 : FROM node:12-alpine
12-alpine: Pulling from library/node
...
Status: Downloaded newer image for node:12-alpine
---> 2f014773d54a
Step 2/6 : RUN apk add --no-cache python3 g++ make
...
---> e6f3f854c716
Step 3/6 : WORKDIR /app
---> Running in 3e5816135320
Removing intermediate container 3e5816135320
---> f022642496e7
Step 4/6 : COPY . .
---> e12722c244a5
Step 5/6 : RUN yarn install --production
...
---> f30eb81550d0
Step 6/6 : CMD ["node", "src/index.js"]
---> Running in 6ba0df0212f7
Removing intermediate container 6ba0df0212f7
---> 175c344dd6b8
Successfully built 175c344dd6b8
Successfully tagged getting-started:latest
- 命令执行后,按照 Dockerfile 文件指令,首先拉取 node:12-alpine 镜像,因为我们要创建的镜像是基于 node:12-alpine 镜像的;
- 镜像拉取完成后,使用 apk 安装额外的包,python3、g++、make;
- 使用 COPY 服务项目内容至镜像的 app 目录;
- 运行 yarn 安装 Node.js 项目依赖;
- 使用 CMD 指定该镜像应用的执行入口;
- -t 参数指定编译好的镜像名称;
- 最后的 . 参数表明 Dockerfile 文件位于当前目录。
命令执行完,可通过 docker images 查看编译好的镜像:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
getting-started latest 175c344dd6b8 16 minutes ago 410MB
运行容器
$ docker run -dp 3000:3000 getting-started
运行完成后可打开浏览器 http://localhost:3000 地址,可以看到,我们的容易已经正常运行:
更新示例应用
至此,你已经成功将一个 Node.js 项目用 docker 容器运行起来了 ;如果这时,你收到一个需求,需要将页面的上提示文字进行修改。
修改源码
将文件 src/static/js/app.js
第 56 行提示文字修改为:
- <p className="text-center">No items yet! Add one above!</p>
+ <p className="text-center">You have no todo items yet! Add one above!</p>
然后重新编译镜像:
$ docker build -t getting-started .
执行:
$ docker run -dp 3000:3000 getting-started
这时,会有报错:
docker: Error response from daemon:
driver failed programming external connectivity on endpoint affectionate_lalande
(4691fc1a407ceb191cab03cba96ac5d4301d3d41e47df4f99030bc1285421644):
Bind for 0.0.0.0:3000 failed: port is already allocated
这是因为此前运行容器仍在工作,所以 3000 端口已被占用。
删除旧容器
查看正在运行的容器
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
dc6dc484b0dc 175c344dd6b8 "docker-entrypoint.s…" 2 hours ago Up 2 hours 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp pedantic_euclid使用 docker stop 停止容器运行
$ docker stop <the-container-id>
当容器停止之后,可以使用 docker rm 删除容器
$ docker rm <the-container-id>
如果希望使用一条命令删除正在运行的容器,可添加 -f 参数:
$ docker rm -f <the-container-id>
分享应用镜像
分享应用镜像需要使用 docker 镜像仓库;通常 Docker Hub 是默认的镜像仓库,也是我们默认获取其他镜像的地方。
要使用 Docker Hub,必须先注册 Docker ID。
创建镜像源
在发布镜像之前,首先需要在 Docker Hub 上创建镜像源:
- 注册并登录 Docker Hub ;
- 点击 Create Repository 按钮;
- 源名称为 getting-started,Visibility 选择 Public;
- 点击 Create 按钮。
推送镜像
登录 Docker Hub:
$ docker login -u YOUR-USER-NAME
使用 Docker ID 对镜像设置标签:
$ docker tag getting-started YOUR-USER-NAME/getting-started
推送镜像:
$ docker push YOUR-USER-NAME/getting-started
永久存储
容器文件系统
即使创建于同一个镜像,不同的容器包含独立的文件系统。
试验
创建一个包含
/data.txt
文件的 ubuntu 容器,/data.txt
中的内容是 1 到 10000 的随机数据;$ docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"
验证容器中
/data.txt
文件的内容:$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
89699b139c9d ubuntu "bash -c 'shuf -i 1-…" 11 seconds ago Up 10 seconds fervent_proskuriakova
$ docker exec 89699b139c9d cat /data.txt
6304基于 ubuntu 镜像再创建另外一个容器:
$ docker run -it ubuntu ls /
bin dev home lib32 libx32 mnt proc run srv tmp var
boot etc lib lib64 media opt root sbin sys usr
可以看到,在第二个容器中,并没有 /data.txt
文件,这说明每个容器的文件系统都是独立的。
容器 volume
由上面的试验可知,容器的文件系统相互独立,但是使用 volumes 可以将容器中的文件映射至宿主机上,这样即使容器重启,依然能看到此前创建的文件。
永久存储数据示例
创建 volume:
$ docker volume create todo-db
使用 volume 创建容器:
$ docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started
这时的容器已经将存储的数据通过 volume 映射至宿主机,即使将容器删除再重新创建,数据也不会丢失!
深入了解 volume
你一定很好奇 volume 实际存储在哪里?通过命令 docker volume inspect 则可以查看:
$ docker volume ls
DRIVER VOLUME NAME
local todo-db
$ docker volume inspect todo-db
[
{
"CreatedAt": "2021-12-14T15:58:47+08:00",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
"Name": "todo-db",
"Options": {},
"Scope": "local"
}
]
bind mounts
在之前的例子中,我们使用了 volume 来实现数据的永久存储;但是 volume 在宿主机的存储位置是自动分配,并不能由我们自己指定;但有时候我们需要自己来指定 volume 在宿主机的存储位置,这样当文件发生改变时,可以将文件内容直接反应至容器;例如在开发的过程中,对源文件的修改能及时通过测试容器查看,而不是每次都重新编辑镜像以及运行容器。
创建开发模式下的容器
在 Node.js (/home/cooper/getting-started/app
)项目目录中运行以下命令:
$ docker run -dp 3000:3000 \
-w /app -v "$(pwd):/app" \
node:12-alpine \
sh -c "yarn install && yarn run dev"
该命令直接运行容器,并没有编译镜像,这样很适合处于开发模式的场景。
- -dp 3000:3000 - 在后台运行容器,将宿主机的 3000 端口映射至容器的 3000 端口;
- -w /app - 设定容器 /app 目录为工作目录;
- -v "$(pwd):/app" - 使用 bind mounts 方式将宿主机当前目录挂载至 容器的 /app 目录;
- node:12-alpine - 项目依赖的镜像;
- sh -c "yarn install && yarn run dev" - 在容器中执行命令。
容器运行时,可以通过 docker logs -f <container-id>
来查看日志:
$ docker logs -f 941530b27fbf
...
$ nodemon src/index.js
[nodemon] 2.0.13
[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
此时,修改源文件后,刷新页面就可以看到变化!
多容器应用
截止到目前,我们的应用只使用到一个容器,但是很多时候都需要同时使用多个容器,比如应用需要连接到数据库 MySQL,那么 MySQL 需要运行在单独的一个容器里面。
如果两个容器位于同一个网络中,他们之间可以通信。
创建 MySQL 容器
创建网络
$ docker network create todo-app
通过 ls 可以查看已创建的网络:
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
b4b3a40366ae bridge bridge local
b3e0ff4d6425 host host local
5c7eb6ab0210 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:5.7
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6b364101890f mysql:5.7 "docker-entrypoint.s…" About a minute ago Up About a minute 3306/tcp, 33060/tcp laughing_golick
这里将 todo-mysql-data 映射到了容器的路径 /var/lib/mysql
,但是我并没有提前通过命令 docker volume create
创建该 volume,这是因为 docker 本身会识别并自动创建 volume。
确认 MySQL 数据库已经创建完成:
docker exec -it <mysql-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 数据库已成功创建!
连接应用至 MySQL 数据库
$ 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"
这里的 MYSQL_HOST=mysql
对应于创建 MySQL 容器时的 --network-alias mysql
。
从日志可以看出已经顺利连接上数据库:
$ docker logs <container-id>
[nodemon] 2.0.13
[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
Docker Compose
使用 Docker Compose,可以通过定义一个 YAML 文件来编排多个容器。
创建 compose 文件
在项目的根目录下,创建一个 docker-compose.yml 文件,该文件的开头一般为当前支持的版本:
version: "3.7"
定义应用服务
首先来回忆下此前创建容器的命令:
$ 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"
在 compose 文件中对应的定义如下:
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 服务
创建 MySQK 容器的命令为:
$ 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
对应的 compose 定义:
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:
运行 compose
启动
$ 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
- -d - 表示在后台运行应用
默认情况下,Docker Compose 会针对定义好的多个应用创建统一的网络。
日志
通过 docker-compose logs -f
可以查看运行日志:
mysql_1 | 2019-10-03T03:07:16.083639Z 0 [Note] mysqld: ready for connections.
mysql_1 | Version: '5.7.27' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL)
app_1 | Connected to mysql db at host mysql
app_1 | Listening on port 3000
如果想查看其中某一个服务的日志,使用 docker-compose logs -f app
。
停止
使用 docker-compose down
可停止服务。