参考官方文档Get started
下载并启动教程镜像
先启动Docker Desktop,然后打开终端,输入命令:
docker run -d -p 80:80 docker/getting-started
参数说明:
- -d 以分离模式(detached mode)运行容器(即后台运行容器)
- -p 80:80 将主机的80端口映射到容器的80端口
- docker/getting-started 要使用的容器的名称
第一次输入的时候,提示未运行docker daemon,启动docker desktop之后,第二次运行成功
多个单字符的参数可以合并,例如将-d -p 80:80简化为-dp 80:80
docker run -dp 80:80 docker/getting-started
容器运行后,通过浏览器打开http://localhost即可看到官方教程
Docker Dashboard(Docker仪表板)
使用Docker Dashboard可以方便地管理容器,如图所示(docker会给容器随机生成一个名称)
体验示例APP
在本教程的剩余部分,将通过运行一个Node.js的应用程序示例来演示docker如何使用
下载APP代码
可以下载app文件夹的压缩包或将整个仓库克隆下来,文件夹内有一个json和两个子文件夹
创建APP的容器镜像
Dockerfile是一个基于文本的用于创建容器镜像的脚本文件
- 在package.json所在文件夹下(package.json的同级位置)创建一个名为“Dockerfile”的文件,内容如下
# syntax=docker/dockerfile:1
FROM node:12-alpine
RUN apk add --no-cache python2 g++ make
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
EXPOSE 3000
- 在文件夹下打开终端,使用docker build命令创建容器镜像
docker build -t getting-started .
此时会发现正在下载很多“layers”,这是因为在脚本中写了
FROM node:12-alpine
来表示镜像需要从这个镜像开始构建,而电脑里没有这些镜像,所以需要下载。下载好后,通过yarn命令将app的依赖下载下来。
-t后跟参数getting-started表示将这个镜像命名为getting-started
最后的“.”表示docker build命令应该在当前目录下查找Dockerfile
不使用代理的话,下载起来可能会很慢,两种办法,换源或者使用代理
若使用代理,则
- 方法一:在运行命令的时候添加参数(不能为127.0.0.1,因为docker会使用自己创建的网卡,所以需要将代理地址改为本机ip,然后在代理软件里设置监听地址为0.0.0.0,Ubuntu软件商店的Electron不能设置监听地址,可以使用商店里的Qv2ray):
--build-arg http_proxy=http://192.168.66.23:1080 --build-arg https_proxy=http://192.168.66.23:1080
- 方法二:在~/.docker/config.json中添加
"proxies": { "default": { "httpProxy": "http://192.168.66.23:1080", "httpsProxy": "http://192.168.66.23:1080", "noProxy": "localhost" } },
如图所示
运行的时候不需要加参数,docker会自动配置容器的代理
- 方法三:
方法一适用于docker build命令,若docker run命令下载镜像时,在Docker Desktop的设置中设置代理即可
- 若换源,则参考菜鸟教程
启动APP容器
使用docker run命令来启动
docker run -dp 3000:3000 getting-started
-d -p 这两个参数在上个博客中已经介绍过了,一个是后台运行,一个是指定端口映射。如果没有端口映射,就无法访问app。- 运行后,在浏览器中输入地址http://localhost:3000就可以看到app,界面如图所示
- 在New Item栏输入内容,然后点击Add Item添加,可以看到已经加入了内容,点击左侧的方框可以标记内容,点击右侧的红色垃圾桶可以删除内容
- 此时打开Docker DashBoard,可以看到正在运行的app容器
接下来学习如何更新app并且用新的镜像来更新我们正在运行的app
更新APP
以将页面中的文字由
No items yet! Add one above!更换为
You have no todo items yet! Add one above!
为例
更新源代码
- 在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 build -t getting-started . --build-arg http_proxy=http://192.168.66.23:1080 --build-arg https_proxy=http://192.168.66.23:1080
- 运行容器
docker run -dp 3000:3000 getting-started
会报错
这是因为旧容器还在运行,占用了3000端口,要解决这个问题,需要删除旧容器
删除旧容器
要删除一个容器,首先需要停止运行这个容器。当它停止运行后,有两种方法删除旧容器,任选一个即可。
方法一:使用命令删除容器
使用docker ps命令获取容器的ID
docker ps
使用docker stop命令停止容器
docker stop 容器ID
使用docker rm命令删除容器
docker rm 容器ID
可以将步骤2和3简化为一个步骤,即使用docker rm -f命令删除容器
docker rm -f 容器ID
方法二:使用Docker Dashboard删除容器
- 打开Dashboard,找到要删除的容器
- 点击右侧的删除
点击Delete Forever确认删除即可
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/929fe4fda68748898978550e6daace51.png#pic_center)
启动刚才更新了APP的容器
执行命令
docker run -dp 3000:3000 getting-started
- 刷新浏览器页面或重新打开http://localhost:3000/即可看到更新成功
小结
以上的更新APP的方法存在两个问题:
- 更新完后所有的Item都丢失了,即原先的数据都丢失了。
- 为了仅仅实现将这么小的一个改动部署,却执行了很多步。
共享APP
Docker Hub 是全世界最大的Docker容器镜像仓库和社区,如果没有账号的话,需要 注册Docker ID 才能登录
初始化用户
docker desktop使用 pass 存储gpg密钥,从Docker Dashboard或Docker menu登录Docker Hub前,必须初始化pass。生成gpg密钥后,就可以实现 docker login 的时候不输入密码,使用 Docker Hub生成的访问令牌 登录
生成gpg密钥
gpg --generate-key
生成后将输出内容中的pub部分复制
初始化pass
pass init 刚才复制的pub部分
- 登录Docker Desktop或使用docker login命令登录即可,使用docker hub生成的访问令牌登录时,注意保管好令牌,因为生成之后只在生成完成的时候显示一次,登录的时候将输入的密码改为输入生成的令牌即可
若要删除密钥,需要删除公钥和私钥,推荐使用系统应用程序菜单中自带的密码管理器
# 删除私钥
gpg --delete-secret-keys 私钥
# 删除公钥
gpg --delete-keys 公钥
若要删除pass中的密码信息
pass rm gpg生成密钥时使用的邮箱
创建一个仓库
为了能够推送(push)镜像,首先需要在Docker Hub上创建一个仓库
- 登录Docker Hub
- 点击创建仓库按钮“Create a Repository”
使用“getting-started”作为仓库名称,并且将可访问性设置为公开,然后点击创建按钮“Create”
推送镜像
打开命令行,输入仓库显示的推送命令
docker push 用户名/getting-started
例如用户名为docker,则推送命令为
docker push docker/getting-started
上面的推送命令会执行失败,这是因为这条命令会查找名为 docker/getting-started 的镜像,但是并没有找到。如果运行 docker image ls 命令,会发现列出的镜像里也没有这个镜像。(如果之前在 下载并启动教程镜像 这一部分中下载了教程镜像,则这个命令会开始执行,并且使用 docker image ls 命令会列出该镜像)
使用命令登录 Docker Hub
docker login -u 用户名
使用 docker tag 命令给getting-started镜像命名一个新名称
docker tag getting-started 用户名/getting-started
现在尝试重新推送镜像,如果是从 Docker Hub 复制的命令:
docker push 用户名/getting-started:tagname
那么现在不需要输入tagname部分,只需使用 1. 中的命令即可,因为并没有添加tagname。如果推送的时候不指定tag,Docker将使用名为latest的tag
在新的实例上运行镜像
- 打开 Play with Docker 并登录
- 点击 ADD NEW INSTANCE 添加实例
在右侧的终端中输入命令
docker run -dp 3000:3000 你的用户名/getting-started
- 点击右上角的open port输入3000,或点击3000按钮,即可看到更新的界面
小结
在这一部分中,我们学习了如何将镜像推送到仓库来分享我们的镜像,并在全新的实例中运行推送的最新镜像。接下来我们将学习如何在重启容器后还能保持数据。
保留容器的数据(库)
每次我们启动容器的时候,todo list中的数据就会被清除,为什么会是这种情况?让我们研究一下容器是如何工作的。
容器的文件系统
当一个容器运行的时候,它会使用一个镜像中的很多layers来作为它的文件系统。每个容器也有自己的“暂存空间”(scratch space)用于创建、升级、删除文件。任何改变都不会在别的容器中看到,及时它们使用的是相同的镜像。
- 通过练习查看容器的文件系统
我们将启动两个容器并各自创建一个文件。你将看到一个容器中创建的文件在另一个容器中无法使用。
启动 ubuntu 镜像并把1~10000之间的一个随机数写入一个名为data.txt的文件
docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"
我们通过 && 以使用两个命令,第一个命令为生成随机数并写入data.txt中,第二个命令只是监视文件以保持容器运行(The second command is simply watching a file to keep the container running.)
- 点击 open in terminal
在打开的终端中输入以下命令以查看文件内容
cat /data.txt
如果想在自己的终端中运行命令,则使用以下命令(容器ID需要使用 docker ps 命令获取)
docker exec 容器ID cat /data.txt
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/1637ca285d754d2fbfed77e311631579.png#pic_center)
同样可以看到生成的随机数
使用以下命令删除刚才的容器
docker rm -f 容器ID
容器卷(Container volumes)
在先前的实验中,我们看到每个容器每次启动时都从镜像定义开始(we saw that each container starts from the image definition each time it starts)。尽管容器可以创建、更新和删除文件,这些变动都会在容器删除时丢失并且容器间所有的改变都是独立的。使用卷,我们可以改变这些情况。
卷为容器提供了连接主机的特定路径下的文件系统的能力(Volumes provide the ability to connect specific filesystem paths of the container back to the host machine.)。如果容器中的一个目录被挂载,这个目录中的变动就可以被主机看到。如果我们跨容器重新启动装载相同的目录,我们将看到相同的文件(If we mount that same directory across container restarts, we’d see the same files.)。
卷主要有两种类型。这两种类型我们最后都会使用,但是开始的时候会用命名卷(named volumes)。
使用命名卷(named volume)保留todo数据
todo app默认将它的数据使用 SQLite Database 存储在容器的文件系统内的 /etc/todos/todo.db 下。这是一种简单的关系型数据库,所有的数据都存在一个文件中,稍后我们将讨论如何将其切换到其他数据库引擎。
使用 docker volume create 命令创建一个卷
docker volume create todo-db
- 在Docker Dashboard中停止并删除todo app容器(或使用 docker rm -f 命令)
启动todo app容器,但在启动时加入 -v 参数来指定要挂载的卷。我们将使用命名卷并将它挂载到 /etc/todos 。
docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started
- 容器启动后,打开app并在todo list中添加一些项目
- 重复步骤 2. ,删除容器
- 重复步骤 3. ,启动一个新的容器
- 打开app,你会发现你在上个app容器添加的项目还在
进一步了解卷
使用 docker volume inspect 命令查看卷
docker volume inspect todo-db
Mountpoint 是数据实际存储的位置
小结
现在,我们有了一个可以在重启之后继续运行的应用程序!然而在之前的步骤中我们看到,对于每个变动,都需要花费相当一部分时间重新构建镜像以使得变动生效。有一个更好的办法来使这些变动生效,那就是绑定挂载。
使用绑定挂载(bind mounts)
在之前的章节中,我们讲解了使用命名卷(named volume)来保留我们数据库中的数据。如果我们只是简单地存储数据,命名卷会非常好用,因为我们不必担心数据存放的具体位置。
使用绑定挂载,我们可以在主机上指定挂载点,我们可以用这个方法来存储数据,但是它经常用于向容器提供额外的数据。当容器工作时,我们可以使用绑定挂载来向容器挂载我们的源码以使得容器能看到代码的更改、响应并让我们立即看到更改。
快速比较不同类型的卷之间的区别
命名卷(Named Volumes) | 绑定挂载(Bind Mounts) | |
---|---|---|
数据在主机中存放的位置 | 由Docker决定 | 由用户指定 |
挂载示例 (使用 -v ) |
my-volume:/usr/local/data | /path/to/data:/usr/local/data |
用容器内容填充新卷(Populates new volume with container contents) | 支持 | 不支持 |
支持卷驱动程序(Supports Volume Drivers) | 支持 | 不支持 |
启动一个开发模式的容器
为了使我们的容器支持开发流程,我们将执行以下步骤
- 将我们的源码挂载到容器
- 安装所有的依赖,包括“dev”依赖
- 启动 nodemon 以监视文件更改
- 确保你现在没有任何正在运行的 getting-started 容器
在app所在目录下运行以下命令,在之后我们会解释这些命令
对于Linux,运行docker run -dp 3000:3000 \ -w /app -v "$(pwd):/app" \ node:12-alpine \ sh -c "yarn install && yarn run dev"
如果是在Windows,在PowerShell中运行以下命令
docker run -dp 3000:3000 ` -w /app -v "$(pwd):/app" ` node:12-alpine ` sh -c "yarn install && yarn run dev"
如果是在 Apple silicon Mac 或其他 ARM64 设备,运行以下命令
docker run -dp 3000:3000 \ -w /app -v "$(pwd):/app" \ node:12-alpine \ sh -c "apk add --no-cache python2 g++ make && yarn install && yarn run dev"
命令解释:
-dp 3000:3000和之前一样,分离(后台)模式运行并创建一个端口的映射
-w /app
设置运行命令的“工作目录”或当前目录
-v "$(pwd):/app"
将当前目录挂载到容器的 /app 目录
node:12-alpine
要使用的镜像
sh -c "yarn install && yarn run dev"
在容器中要执行的命令。使用sh(alpine 没有bash)运行 yarn install 安装所有依赖,然后运行 yarn run dev。如果看一下 package.json 就会看到 dev 脚本是在启动 nodemon。
使用 docker logs 命令查看输出日志。当你看到这种输出时就可以知道你可以继续接下来的步骤了
docker logs -f 容器ID
如果要关闭查看,按下 Ctrl+C 即可退出
既可以使用docker ps命令查看正在运行的容器的信息,也可以使用 docker container ls 列出所有容器的信息(docker image ls 是列出所有镜像)
- 现在对app进行修改。在 src/static/js/app.js 文件中,将109行的 “Add Item”改为“Add”
- 只需要刷新app页面(或重新打开)就可以立即看到改动。可能需要等待node服务器重启,所以如果页面报错,等待一会然后再刷新,当使用步骤 3. 看到如下输出时,说明成功启动
在后面的教程中我们会介绍 Docker Compose ,这会简化我们的命令(我们已经使用了很多参数了,例如-dp -v -t等)
小结
通过使用绑定挂载,我们可以快速响应需求并发布更改。在接下来的教程中,将会介绍如何在容器中使用MySQL来代替SQLite,并使多个容器互相通信。
多容器的APP
目前为止,我们已经能够使用单个容器运行app。但是现在需要向app添加MySQL,那么就会出现接下来的问题——“MySQL应该在哪运行?是安装在app所在的容器中并运行吗?”
总的来说,每个容器应该只能做一件事并做好,理由如下:
- 很有可能您必须以不同于数据库的方式扩展API和前端(There’s a good chance you’d have to scale APIs and front-ends differently than databases)
- 单独的容器允许您独立地进行版本和更新(Separate containers let you version and update versions in isolation)
- 虽然您可以在本地为数据库使用容器,但您可能希望在生产中为数据库使用托管服务。那么,你不想在应用程序中附带数据库引擎。(While you may use a container for the database locally, you may want to use a managed service for the database in production. You don’t want to ship your database engine with your app then.)
- 运行多个进程将需要一个进程管理器(容器只启动一个进程),这会增加容器启动/关闭的复杂性(Running multiple processes will require a process manager (the container only starts one process), which adds complexity to container startup/shutdown)
app的架构将升级如下
容器网络
容器默认情况下是独立运行的,不知道运行在同一机器上的其他进程或容器的任何信息。在同一网络下的两个容器可以互相通信。
启动MySQL
有两种让容器联网的方法:
- 启动时分配
- 连接现有容器
现在我们将创建一个网络并在MySQL容器启动时连接
创建网络
docker network create todo-app
- 启动MySQL容器并连接创建的网络。同时还会设置一些环境变量以供数据库初始化自己时使用(详情见官方文档的“Environment Variables”)
Linux系统使用以下命令
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
如果是基于ARM的芯片,比如M1,使用以下命令
```bash
docker run -d \
--network todo-app --network-alias mysql \
--platform "linux/amd64" \
-v todo-mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=todos \
mysql:5.7
```
如果是Windows,在PowerShell中使用以下命令
```bash
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
```
我们指定了--network-alias参数,一会我们会回过头来讨论这个。
我们正在使用挂载在存放MySQL数据的 /var/lib/mysql 下的名为 todo-mysql-data 的卷。然而我们从未使用 docker volume create 命令。Docker会识别我们想要的命名卷的名称并自动为我们创建这个卷。
为了确认我们已经启动了数据库,使用以下命令连接数据库
docker exec -it MySQL容器的ID mysql -u root -p
输入密码时,输入secret
在MySQL Shell中,列出数据库并确认你看到了 todos 数据库
SHOW DATABASES;
你应该看到类似这种的输出
连接MySQL
使用 nicolaka/netshoot 容器(附带有许多工具,对于解决或调试网络问题非常有用)
新建一个使用 nicolaka/netshoot 的容器,确保它连接到和MySQL容器相同的网络
docker run -it --network todo-app nicolaka/netshoot
在容器中,使用 dig 命令,这是一个很有用的DNS工具
dig mysql
会看到类似如下的输出
在“ANSWER SECTION”中,你会看到MySQL有一个记录 A 解析172.18.0.2。虽然mysql通常不是有效的主机名,但Docker能够将其解析为具有该网络别名的容器的IP地址(因为之前使用了--network alias参数)。这意味着我们的app只需要连接到名为mysql的主机就可以和数据库通信了。
使用MySQL运行app
todo app支持使用以下环境变量来设置MySQL的连接:
- MYSQL_HOST
运行MySQL服务器的主机名
- MYSQL_USER
连接MySQL使用的用户名
- MYSQL_PASSWORD
连接MySQL使用的密码
- MYSQL_DB
要使用的数据库的名称
注意:对于8.0及以上版本的MySQL,确保在mysql中运行以下命令(Note: for MySQL versions 8.0 and higher, make sure to include the following commands in mysql)
ALTER USER 'root' IDENTIFIED WITH mysql_native_password BY 'secret'; flush privileges;
设置上面的每一个环境变量,并将容器连接到app网络(同样在app所在目录下执行)
Linux系统运行以下命令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"
如果使用的Windows,在PowerShell中运行以下命令
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"
- 使用 docker logs 容器ID 命令会看到类似如下的输出
- 打开app,添加一些item
连接数据库,密码secret
docker exec -it MySQL容器ID mysql -p todos
然后在mysql shell中输入以下命令
select * from todo_items;
可以看到以下输出
打开Docker DashBoard,可以看到这两个容器都在运行,但没有迹象表明它们在一个应用程序中组合在一起,接下来就会介绍如何使用 Docker Compose 改善这种情况。
小结
在这部分教程中,我们实现了一个将数据存储在另一个容器中的应用程序。我们学习了一点关于容器网络的内容并看到了如何使用DNS执行服务发现。
在下一个教程中,我们将介绍 Docker Compose。通过 Docker Compose 可以以一种非常容易的方式将我们的应用程序分享出去,并且其他人只需要一个命令就能启动它们。
使用Docker Compose
Docker Compose 是一个用于帮助定义并分享多容器应用程序的工具。使用Compose,我们可以创建一个YAML文件来定义服务,并且只需一个命令就能启用或关闭它们的每一个。
使用Compose最大的优点就是你可以在一个文件中定义你的应用程序栈,将这个文件放在你的项目仓库的根目录。其他用户只需要clone你的仓库并启动compose程序就可以使用。
安装Docker Compose
按照官方文档的说法,如果你在Windows或Mac上安装了 Docker Desktop 或 Docker Toolbox ,你就已经有了 Docker Compose 。 Play-with-Docker 实例同样已经安装了 Docker Compose 。如果你使用的是Linux设备,你需要安装 Docker Compose 实际上,Linux上如果安装了Docker Desktop也不需要再手动安装 Docker Compose。
文档中给的查看是否安装的命令是
docker-compose version
但经过实际测试,命令为
docker compose version
创建Compose文件
- 在app项目的根目录,创建一个名为 docker-compose.yml的文件
在Compose文件中,我们将从定义模式版本开始。在大部分情况下,最好使用最新版本。关于不同版本和兼容性的详细信息参见官方文档 compose-versioning
version: "3.7"
下一步,我们将定义作为应用程序的一部分运行的服务(或容器)列表
version: "3.7" services:
定义应用程序服务
这是我们之前定义容器的时候所用的命令
Linux使用
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"
Windows在PowerShell中使用
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服务时会很有用。
version: "3.7"
services:
app:
image: node:12-alpine
- 使用 command 定义要执行的命令
version: "3.7"
services:
app:
image: node:12-alpine
command: sh -c "yarn install && yarn run dev"
- 使用 ports 定义端口映射
version: "3.7"
services:
app:
image: node:12-alpine
command: sh -c "yarn install && yarn run dev"
ports:
- 3000:3000
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 定义环境变量
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服务
在之前的教程中使用以下命令运行容器
Linux下使用
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
Windows下在PowerShell中使用
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
- 定义名为 mysql 的服务,这样它就会自动获得网络别名。然后定义镜像。
version: "3.7"
services:
app:
# The app service definition
mysql:
image: mysql:5.7
- 接下来使用 volumes 定义卷映射,如果只是简单的提供卷的名称,则compose使用的是默认选项。定义更多内容参见Volume configuration reference
version: "3.7"
services:
app:
# The app service definition
mysql:
image: mysql:5.7
volumes:
- todo-mysql-data:/var/lib/mysql
volumes:
todo-mysql-data:
- 最后指定环境变量
version: "3.7"
services:
app:
# The app service definition
mysql:
image: mysql:5.7
volumes:
- todo-mysql-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: todos
volumes:
todo-mysql-data:
现在,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:
运行应用程序栈
- 确保没有正在运行的其他容器
使用 docker compose up 命令运行应用程序,-d 参数意为后台运行所有内容
docker compose up -d
Docker Compose 会自动为应用程序栈创建相应名称的网络(这就是为什么我们没有在compose文件中定义一个网络)
现在通过 docker compose logs -f 命令查看日志,你将在一个流里面看到每个服务实时输出的日志,因为 -f 参数意为“follow”,所以会提供实时输出。
docker compose logs -f
如果要查看特定服务的输出,在 -f 后加上服务的名称即可
在 Docker DashBoard 中查看应用程序栈
可以看到有一个名为app的分组。默认情况下,docker-compose.yml所在的文件夹的名称(即项目名称)会作为分组名称。
点击进入分组,里面所含的容器的名称也很有描述性,遵循
<project-name>_<service-name>_<replica-number>
即:项目名称_服务名称_副本编号 的命名方式。
删除容器组(包含其中的容器)和网络
使用 docker compose down 命令或点击 Docker Dashboard 中分组栏的垃圾桶图标来删除,默认情况下两种方式都不会删除创建的卷,如果需要删除相应的卷,在命令后加上 --volumes 参数或点击 Docker Dashboard 的 Volumes 栏找到卷并删除
镜像构建最佳实践
安全扫描
构建镜像后,最好使用 docker scan命令扫描它以查找安全漏洞。Docker 与 Snyk 合作提供漏洞扫描服务。需要使用 docker scan --login 登录后才能使用扫描。
扫描结果的输出会列出漏洞的类型、了解更多信息的 URL,以及重要的是相关库的哪个版本修复了漏洞。
查看镜像层(layer)
使用 docker image history 可以查看用于在镜像中创建每个镜像层的命令,添加 --no-trunc 参数可以获得完整输出,例如
docker image history --no-trunc getting-started
镜像层缓存
使用缓存可以减少容器构建时间。因为一旦某个层发生变化,所有的下层也会被重新构建。
回顾之前为getting-started创建的Dockerfile
# syntax=docker/dockerfile:1
FROM node:12-alpine
RUN apk add --no-cache python2 g++ make
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
EXPOSE 3000
再回想 docker image history 的输出,可以看到Dockerfile中的每个命令最后都成了镜像的一层。你可能还记得每次我们向镜像中做出变动,yarn的依赖就会被重新安装一次。
为了解决这个问题,我们需要重构我们的Dockerfile以便支持缓存依赖。对于基于node应用程序,这些依赖都位于package.json中。
- 首先将package.json复制进Dockerfile,然后再复制剩下的所有内容
# syntax=docker/dockerfile:1
FROM node:12-alpine
RUN apk add --no-cache python2 g++ make
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --production
COPY . .
CMD ["node", "src/index.js"]
EXPOSE 3000
- 在Dockerfile所在目录下创建一个 .dockerignore 文件,写入以下内容
node_modules
- 使用 docker build 命令构建镜像
docker build -t getting-started .
- 现在改变 src/static/index.html 中的内容(比如将 \<title> 中的内容改为“The Awesome Todo App”)
- 重复步骤 3. ,你将看到类似这种的输出
多阶段构建
Maven/Tomcat 示例
在构建基于 Java 的应用程序时,需要一个 JDK 来将源代码编译为 Java 字节码。但是,生产中不需要 JDK。此外,您可能正在使用 Maven 或 Gradle 等工具来帮助构建应用程序。在我们的最终镜像中也不需要这些。多阶段构建可以帮助解决这个问题。
# syntax=docker/dockerfile:1
FROM maven AS build
WORKDIR /app
COPY . .
RUN mvn package
FROM tomcat
COPY --from=build /app/target/file.war /usr/local/tomcat/webapps
在此示例中,我们使用一个阶段(称为build)来使用 Maven 执行实际的 Java 构建。在第二阶段(从 FROM tomcat 开始),我们从 build 阶段复制文件。最终镜像只在最后阶段构建(可以使用 --target 标志覆盖)。
React 示例
在构建 React 应用程序时,我们需要一个 Node 环境来将 JS 代码(通常是 JSX)、SASS 样式表等编译成静态 HTML、JS 和 CSS。如果我们不进行服务器端渲染,我们甚至不需要 Node 环境来进行生产构建。为什么不在静态 nginx 容器中发送静态资源?
# syntax=docker/dockerfile:1
FROM node:12 AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
在这里,我们用了 node:12 镜像来构建(最大化层缓存),然后将输出复制到Nginx容器中
回顾
通过简单了解镜像的结构,我们可以更快地构建镜像并减少更改。扫描镜像让我们确保我们正在运行和分享的容器是安全的。多阶段构建还能通过将构建时依赖项与运行时依赖项分开,帮助我们减少整体镜像大小并提高最终容器的安全性。