面向前端开发人员的 Docker:用于开发的自定义 Docker 镜像
作者:本杰明·马丁
让我们花点时间思考一下,对于本地开发来说,什么才是最重要的。对我来说,我希望确保所有开发人员都使用相同的依赖项,并且不必担心他们安装了什么版本。不再需要“但它在我的机器上可以运行”这样的借口。同时,我希望确保我们保留 HMR(热模块替换)的便利性,这样开发人员就无需不断刷新应用程序就能看到他们的更改。我们不想失去快速的反馈。
在本文中,我们将研究如何为具有自定义的样板 VueJS 应用程序设置 Docker, Dockerfile
从中构建我们的图像和容器,以及如何从中获得效率。
如果您错过了本系列的第一部分, 请点击此处了解更多关于 Docker 命令行界面的信息。本节需要使用第一部分中的命令。如果您已经熟悉 Docker CLI,请继续阅读。
先决条件:创建我们的项目
这当然是一篇关于 Docker 的文章,所以请确保你已经安装了 Docker。你可以按照 Docker 的官方安装说明进行操作。由于我使用的是 Vue,所以我使用 VueCLI 快速启动了一个工作区 vue create docker-demo
。
我选择的配置(如下所示)将与 E2E 测试和单元测试相关,它们将成为我们 CI/CD 管道的一部分。
一旦所有东西都安装完毕, cd
进入我们的新项目文件夹,打开一个 IDE,让我们开始吧。
用于开发的自定义 Docker 镜像
如果您使用过 Docker 但从未构建过自己的镜像,您可能知道我们在执行 docker run
命令时会指定一个镜像。这些镜像是从 Docker Hub 或其他远程仓库(如果本地找不到该镜像)拉取的。但在本例中,我们想要构建一个自定义镜像。
在项目根目录中,创建一个名为 的文件 Dockerfile.dev
。这将是我们的开发镜像。在该文件中,将以下代码复制到其中。
# Base Image
FROM node:9.11.1
ENV NODE_ENV=development
ENV PORT=8080
WORKDIR /usr/src/app
COPY package*.json /usr/src/app/
RUN cd /usr/src/app && CI=true npm install
EXPOSE 8080
CMD ["npm", "run", "serve"]
好吧……但这都有什么用呢?让我们深入研究一下。
Dockerfile 命令和关键字
FROM
指定构建自定义镜像时所基于的现有镜像。由于我们运行的是 Node.js 应用程序,因此我们选择了其官方 Docker 镜像之一。
FROM node:9.11.1 表示我们的应用程序映像将从节点 v 9.11.1 映像开始
ENV
设置环境变量
ENV PORT=8080
设置环境变量PORT
以供稍后使用
ENV NODE_ENV=development
NODE_ENV
设置应用程序中使用的 环境变量
WORKDIR
设置容器内的工作目录
WORKDIR /usr/src/app
定义/usr/src/app/
为 Docker 镜像中的工作目录
COPY
将新文件、目录或远程文件复制到容器/镜像中
COPY package*.json /usr/src/app/
将我们的package.json
和package-lock.json
复制到我们的工作目录中
RUN
在当前镜像之上的新层中执行命令并提交。运行构建时,您将看到一个哈希值,代表最终镜像的每一层
RUN cd /usr/src/app/ && CI=true npm install
将工作目录更改为package.json
所在的位置,并将所有依赖项安装到镜像中的此文件夹中。这使得镜像保存了依赖项的冻结副本。我们的 Docker 镜像(而不是主机)负责管理我们的依赖项
EXPOSE
允许我们从主机访问容器上的端口
EXPOSE 8080
与容器内运行应用程序的端口匹配,并允许我们从主机访问我们的应用程序
CMD
提供容器创建时运行的默认初始化命令,类似启动脚本
CMD ["npm", "run", "serve"]
将其设置为启动容器时的默认命令。该命令在构建镜像时不会运行,它仅定义容器启动时应该运行的命令。
我知道你急着想让这一切顺利进行,但请稍安勿躁。让我们 仔细 看看Dockerfile.dev
我们 为什么 要这么做。
Dockerfile 结构建议
那么, 我的应用程序在哪里?
对。我们没有使用 命令复制整个工作区。如果这样做,每次代码修改 后 COPY
都需要运行 。我们不想在开发过程中一遍又一遍地重复这个过程。这样可以提高效率。docker build
docker run
缓存依赖项
我们利用了 Docker 的镜像分层机制。Docker 构建镜像时,您会看到每个完成的层的哈希值。更重要的是,Docker 还会缓存这些层。如果 Docker 发现该层与上次构建相比没有任何变化(并且之前的层也完全相同),那么 Docker 就会使用该层的缓存版本,从而节省您和您的开发人员的宝贵时间!当某个层发生变化时,其上的所有缓存层都将失效并被重新构建。
package.json
因此,如果我们的或 没有改变 ,package-lock.json
那么我们的整个图像是可缓存的,不需要重建!
优先事项
这也是为什么我们需要将其他 Dockerfile
不太频繁更改的命令放在文件顶部的原因。一旦缓存的某一层失效(例如,切换 ENV PORT=8080
到另一个端口),该缓存层及其之后的所有缓存层都会失效,Docker 将不得不重建这些缓存层。
构建自定义 Docker 镜像
现在,使用以下命令构建图像: docker build --tag docker_demo:latest --file Dockerfile.dev .
--tag
在命令中 使用 可以让我们轻松地从 命令docker build
中引用该图像docker run
命令末尾
.
的docker build
引用了我们的自定义项所在的上下文Dockerfile
。因此,此命令应从项目目录的根目录运行。
您可以使用来运行它 docker run docker_demo:latest
,但不幸的是,我们还需要做更多的工作才能让它从命令行快速轻松地运行。
运行我们的容器:生活质量的提升
我们将 docker run
每天执行该命令,甚至更频繁。但是,如果我们直接执行该 docker run docker_demo:latest
命令,Docker 每次都会创建一个 新 容器。除非你明确地停止旧容器,否则 Docker 不会停止旧容器。这在很多情况下非常有用,但由于我们对主机端口进行了硬编码,因此在主机上可能会遇到端口冲突。
为了方便我们停止和移除旧容器,我们应该给它们命名,以便以后轻松引用。此外,我希望在取消正在运行的进程时,正在运行的容器也能被移除。
docker run --rm -it\
--name docker_demo_container\
docker_demo:latest
添加了什么?
我们 --name
在运行命令的末尾添加了一个字段。这样我们就可以引用容器,而无需查找哈希值。现在,我们可以轻松地通过名称来停止容器。
我们还在 命令 中 添加了--rm
和 标志 。该 标志指示 Docker 在容器停止时移除容器。该 标志指示容器启动后,终端仍保持活动状态并可交互。-it
docker run
--rm
-it
挂载主机目录
让我们回到 docker run
命令,找到将工作区目录挂载到容器内文件夹的方法。我们可以通过在命令中添加容器的挂载点来实现 docker run
。这将告诉 Docker,我们想要在主机文件夹 ( src
) 和 Docker 容器文件夹 ( dst
) 之间创建一个活动链接。我们的新命令应该如下所示:
docker run --rm -it\
--name docker_demo_container\
--mount type=bind,src=`pwd`,dst=/usr/src/app\
docker_demo:latest
但这可能会与主机 node_modules
文件夹冲突,因为我们要将整个文件挂载 pwd
到镜像中应用程序的位置(以防我们的某个开发人员意外地 npm install
在其主机上运行)。因此,让我们添加一个卷,以确保我们保留 node_modules
容器中存在的文件。
docker run --rm -it\
--name docker_demo_container\
--mount type=bind,src=`pwd`,dst=/usr/src/app\
--volume /usr/src/app/node_modules\
docker_demo:latest
访问容器内的端口
如果您尝试了上述命令(并且正在运行 VueJS 应用程序),您应该会看到:
App running at:
- Local: http://localhost:8080/
It seems you are running Vue CLI inside a container.
Access the dev server via http://localhost:<your container's external mapped port>/
Docker 提示我们需要从容器中暴露一个端口,并将其发布到主机上。我们可以 --publish
在 run 命令中添加标志来实现这一点。(我们 EXPOSE
的 中已经有这个命令 了Dockerfile.dev
)
--publish <host-port>:<container-port>
告诉 Docker,到主机端口(即通过 localhost)的流量 应该被导向 您定义的<host-port>
容器 。<container-port>
docker run
在一个命令中
让我们看一下最终的运行命令:
docker run --rm -it\
--name docker_demo_container\
--publish 4200:8080\
--mount type=bind,src=`pwd`,dst=/usr/src/app\
--volume /usr/src/app/node_modules\
docker_demo:latest
运行上述命令最终将允许我们通过 http://localhost:4200访问我们的应用程序。
测试一下
让我们创建一个新的副本并运行它。如果您尝试更改我们文件的某个模板,您会发现一切仍然正常运行。
但是说到测试,单元测试怎么样?好吧,一旦我们的容器运行起来,我们就可以打开一个新的终端和 docker exec
一个在容器中运行的命令。
docker exec -it docker_demo_container npm run test:unit
上述命令将与我们的容器创建一个交互式终端连接 docker_demo_container
并在其中执行命令 npm run test:unit
,从而允许我们为我们的应用程序运行单元测试。
结束语
现在,我们可以构建开发镜像并在本地运行,同时保留热模块替换的便利性,从而保持高效的开发工作流程。开发人员无需担心主机上的依赖项与镜像中的依赖项发生冲突。再也不用担心“但它在我的机器上可以运行”这样的借口了。此外,我们还拥有一个可以轻松运行的命令来执行单元测试。
如果您发现我遗漏了任何内容或想进一步了解 Docker,请联系我!
鏂囩珷鏉ユ簮锛�https://dev.to/rangle/docker-for-frontend-devs-custom-docker-images-for-development-1afc