【CI/CD】前端开发 Docker 入门

1,401 阅读19分钟

目标问题

  • Docker 是做什么的?
  • Docker 的使用场景是什么?
  • Docker 对于前端来说,有哪些帮助?

行文思路

​ 笔者是一名有一定工作经验的前端开发,此前没用接触过 Docker ,仅仅听说过是它是一个很好用的工具,和环境、部署相关。

​ 因此,本文仅仅是个人对 Docker 的探索和理解,需要辩证地去看待。如果文章出现错误,也欢迎大家在评论区指出,共同探讨学习。同时,文章还会保留,个人在学习过程中的思考和疑惑,希望大家多多指点。

从安装开始

Win11

​ 笔者操作系统为 win11 ,因此仅对此做些介绍。

官网下载并安装

​ 这是我们依据经验, 首先会实施的步骤。

Docker官网

​ 下载到对应版本之后,根据指引安装完毕,我们会多一个Docker Desktop 的应用,并且它会让我们重启一次电脑。

​ 重启之后,运行 Docker Desktop ,但并不能直接使用,。会出一个小黑框,提示我们按任意键安装一个叫 wsl 的东西。

​ 但似乎是因为网络原因,安装的并不顺利。不要着急,可以先取消安装它。通过查阅资料,先让我们看看 wsl 是什么东西?

实际操作中,可以优先安装 wsl2 环境

wsl

什么是 wsl

​ WSL是Windows Subsystem for Linux(Windows 上的 Linux 子系统)的缩写。它是一种Windows操作系统上的功能,允许用户在Windows系统中运行Linux应用程序和工具。WSL包括两个子系统:WSL 1和WSL 2。

​ WSL 1是一个兼容性子系统,它将Linux系统调用转换为Windows系统调用,允许用户在Windows上运行大多数Linux命令行工具和一些应用程序。WSL 1提供了基本的Linux环境,但不支持Linux内核特性。

​ WSL 2则更加强大,它使用真正的Linux内核虚拟机来运行Linux子系统,提供了更好的性能和完整的Linux兼容性。WSL 2还允许用户在Windows和Linux文件系统之间进行直接的访问,使得在Windows上使用Linux工具变得更加便捷。

​ 总体来说,WSL使得在Windows上运行Linux应用程序和工具变得更加容易和高效,对于需要同时使用Windows和Linux的用户来说,这是一个非常方便的功能。

要使用 docker 下 wsl1 还是 wsl2?

​ 在 Windows 上使用 Docker,建议使用 WSL 2,因为 WSL 2 比 WSL 1 具有更好的性能和完整的 Linux 兼容性,可以提供更好的 Docker 使用体验。

​ 使用 WSL 2 后,Docker 将在 WSL 2 的 Linux 子系统中运行,可以直接访问 Linux 文件系统,这样可以避免在 Windows 和 Linux 之间进行文件转换,提高了 Docker 的性能和稳定性。另外,WSL 2 支持使用 Docker Desktop,这样就可以更加方便地管理 Docker 容器和镜像。

​ 虽然 Docker 也可以在 WSL 1 上运行,但是在 WSL 1 中使用 Docker 时,需要在 Windows 上安装 Docker Desktop,然后将 Docker 运行在 Windows 上,这样会导致性能下降,并且可能会出现一些兼容性问题。因此,建议在 Windows 上使用 Docker 时,使用 WSL 2。

开启 wsl2

  1. 开始搜索框,输入powershell,右键选择使用管理员身份运行。

  2. 执行:

    dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
    
  3. 执行:

    dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
    
  4. 执行完毕重启电脑

  5. 下载 wsl_update

  6. 双击安装

  7. 设置版本号

    wsl --set-default-version 2
    
  8. 执行完毕重启电脑

​ 到这里, wsl2 环境已经完毕。重新运行 Docker Desktop ,是正常运作的。但dism.exe 是什么,上述命令是什么意思?

dism.exe

dism.exe 是什么东西?

​ dism.exe 是一个 Windows 操作系统自带的命令行工具,它是部署映像服务和管理(Deployment Image Servicing and Management)工具的缩写,用于管理 Windows 映像文件(Windows Imaging Format,WIM)和虚拟硬盘(Virtual Hard Disk,VHD)。

​ 使用 dism.exe 可以执行多种系统管理任务,例如:

  • 安装、卸载和配置 Windows 功能、驱动程序和语言包
  • 添加、删除和修改 Windows 更新、修补程序和驱动程序
  • 检查和修复 Windows 映像文件中的错误
  • 创建、捕获和部署 Windows 映像文件

​ dism.exe 可以通过命令行或脚本进行使用,它是一种强大的工具,常用于系统管理员和技术支持人员进行 Windows 系统管理和维护。

上述命令是什么意思?
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

这个命令是用于启用 Windows Subsystem for Linux 功能的,具体解释如下:

  • dism.exe:启动部署映像服务和管理工具。
  • /online:指定当前正在运行的操作系统为目标操作系统。
  • /enable-feature:启用一个或多个 Windows 功能。
  • /featurename:Microsoft-Windows-Subsystem-Linux:指定要启用的功能名称为 Microsoft-Windows-Subsystem-Linux,即启用 Windows Subsystem for Linux 功能。
  • /all:启用所有相关的父功能和子功能。
  • /norestart:在启用功能后不要重新启动计算机。

​ 因此,这个命令的作用是在当前运行的 Windows 操作系统中启用 Windows Subsystem for Linux 功能,包括其所有相关的父功能和子功能,并且不需要重新启动计算机。启用了这个功能之后,就可以在 Windows 上运行 Linux 应用程序和工具。

dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

而这个命令是用于启用虚拟机平台功能的,其中还需要的解释如下:

  • /featurename:VirtualMachinePlatform:指定要启用的功能名称为 VirtualMachinePlatform,即启用虚拟机平台功能。

​ 因此,这个命令的作用是在当前运行的 Windows 操作系统中启用虚拟机平台功能。启用了这个功能之后,就可以在 Windows 上运行基于虚拟化技术的应用程序和服务,例如 Docker 和 Kubernetes。虚拟机平台功能还可以用于支持 Windows Subsystem for Linux 2 (WSL2) 的运行,提供更好的性能和完整的 Linux 兼容性。

验证安装

  • 运行 Docker Desktop , 正常工作。

  • 正常输出版本号。

    docker --version
    

用来做什么

​ 安装完毕之后,我们可能陷入了迷茫,因为我们还不知道,哪些问题是需要 Docker 帮助我们的。

​ 我们需要去收集这样的问题和痛点,最好是具体的,与我们切身相关的。

​ 这里借用 给前端的docker 10分钟真 · 快速入门指南 ,描述的场景。

​ 公司有项目A、B、C, 分别使用 node12、node14、node18作为开发环境,并且在多个不同系统的主机上运行。

会存在以下问题:

  • 安装环境费时间
  • 针对不同项目去切换匹配的版本,成本大,且容易混乱
  • 多台服务器部署麻烦,并可能存在系统差异

​ 综上,我们可以感觉到 Docker 比较直观的用处。抹平多系统差异,将项目和环境捆绑,互相隔离独立运行。

核心概念

​ 一开始我们只需要一个粗浅的理解。

​ 这里借用 写给前端的Docker实战教程 - 掘金 (juejin.cn) 的描述

容器(Container)

​ 容器特别像一个虚拟机,容器中运行着一个完整的操作系统。

​ 可以在容器中装 Nodejs,可以执行npm install,可以做一切你当前操作系统能做的事情。

镜像(Image)

​ 镜像是一个文件,它是用来创建 容器 的。

​ 如果你有装过 Windows 操作系统,那么 Docker 镜像特别像“Win7纯净版.rar”文件。

初体验

​ 经过一些简单的了解,我们可以有一个清晰的目标,就是用 Docker 管理我们的项目。

​ 【思考1】: 把一个 项目 做成一个 镜像 ,再由 镜像 生成一个个 容器 。对于主机而言,各自只需要关心 容器 的运行。(推测,不一定准确)

​ 【疑问1】: 能否在 容器 中 运行开发服务 npm run dev ,以及在运行开发环境的必要性。

运行容器

​ 通过查阅 资料 ,结合我司 pnpm monorepo 的项目模板,尝试通过容器npm run dev.

image-20230404133927637.png

​ 组织了如下命令:

docker run -it -d --name v-admin --privileged -p 3000:3000 -v D:/projectCode/zz-platform/v-admin:/app/v-admin  node:16.15.1 /bin/bash -c "node -v && npm i pnpm -g && pnpm -v && cd /app/v-admin/zz-platform-config &&  pnpm i && npm run dev"

​ 这条命令的作用是在一个名为v-admin的容器中运行一个Node.js应用,并将本地的D:/projectCode/zz-platform/zz-platform-config目录挂载到容器的/app/v-admin目录中。同时容器会映射本地的3000端口到容器的3000端口上,并以后台运行的方式启动容器。具体命令中的各个参数含义如下:

  • -it:分配一个伪终端并保持 STDIN 打开。
  • -d:以后台方式运行容器。
  • --name v-admin:为容器指定一个名称。
  • --privileged:以特权模式运行容器。
  • -p 3000:3000:将本地的3000端口映射到容器的3000端口上。
  • -v D:/projectCode/zz-platform/v-admin:/app/v-admin:将本地的D:/projectCode/zz-platform/v-admin目录挂载到容器的/app/v-admin目录中。
  • node:16.15.1:指定要运行的镜像名称和版本号。
  • /bin/bash -c "node -v && npm i pnpm -g && pnpm -v && cd /app/v-admin/zz-platform-config && pnpm i && npm run dev":在容器中执行的命令,查Node.js版本,安装pnpm并检查版本,进入/app/v-admin/zz-platform-config目录,检安装依赖并运行应用。

processon-run2

Docker信息

镜像与容器

​ 运行完上述命令,打开 Docker Desktop ,发现Images列表里多了 node:16.15.1

image-20230404135008478.png

​ 和一个叫 v-admin 的容器,使用 node:16.15.1 在运行。

image-20230404135210286.png

容器状态

​ 通过 view details ,可以查看详情。

image-20230404135552565.png

​ 通过 logs 我们可以了解到, 命令在容器中的运行情况。

​ 通过files 我们可以了解到,本地文件和容器的关系。验证了 容器就像一个虚拟机,运行着一个完整的操作系统。【思考2】上述操作中,本地项目 是被插入到 容器 中运行的,而 镜像 则保证了生成 容器 的环境一致性。

image-20230404141156334.png

​ 我们也确实可以在容器中运行开发服务。这里解答了 我们过程中的 【疑问1】

image-20230404135907579.png

运行开发服务必要性

​ 接下来我们讨论运行开发服务的必要性。

​ 结合Docker的功能,可能可以带来的帮助:

  • 简化本机开发环境的安装

  • 开发环境的统一

​ 实际操作中发现的问题:

  • 开发服务的构建十分缓慢。

    • 具体表现在vite 启动之后,懒加载依赖的效率缓慢。导致页面迟迟不能访问。
  • 编辑器对包路径的识别丢失(可能是由于 pnpm 使用软链的问题造成)

image-20230404143052796.png

  • EslintTypescript 等编辑器开发工具无法使用

  • 代码热更新可能失效(未验证)

  • 项目中额外文件被生成

image-20230404145935588.png

​ 综上,可以认为,容器中运行开发服务是不必要的,至少从我们已有的认知来看。

常用命令

# 查看所有镜像
docker images 
# 命令查看所有容器的状态,找到需要重新执行命令的容器的名称或 ID。
docker ps -a
# 查看容器日志
docker logs <container_name>

更新状态

【疑问2】: 可以修改已经创建的容器映射端口吗?

错误处理

当容器的状态为 Exited (1) 时,说明容器运行过程中发生了错误,导致容器无法正常运行结束。容器在运行时,可以通过以下命令查看容器的运行日志:

docker logs <container_name>

通过查看日志可以确定容器在运行过程中出现了什么错误。根据错误信息来修复容器中的问题,然后重新启动容器即可。如果容器中的问题无法解决,可以使用 docker rm <container_name> 命令删除该容器,然后重新创建容器。

镜像打包

​ 经过 初体验 ,我们强化了 镜像与容器 的概念,学会了如何查看他们,且认为在容器中运行开发服务是非必要的。接下来我们要将目标聚焦在 打包部署 之上。

​ 我们知道镜像可以生成容器,且能保证容器的环境一致。根据 思考1 ,我们可以尝试将 项目 做成一个镜像

​ 如此我们明确前端交付的最终内容:一个包含 项目镜像

​ 而在 初体验 中,我们使用过 node:16.15.1 的镜像,对于一个 项目 的打包部署,除了 node , 我们还需要其他的工具,诸如 pnpmnginx 等。这就好比,我现在自己想做个定制化的系统 Win7定制版.rar,在原来 Win7纯净版.rar的基础上,再安装 微信QQ 这些常用的软件。

​ 我们现在开始尝试打包一个 项目 定制的镜像吧!

镜像打包基础

  1. 在项目根目录下创建一个 Dockerfile 文件,用于定义 Docker 镜像构建规则。
  2. 编写 Dockerfile 文件,指定使用的基础镜像、容器内文件目录和依赖、执行构建命令等。
  3. 在终端中进入项目根目录,执行 docker build 命令进行构建,生成 Docker 镜像。
  4. 将构建好的 Docker 镜像推送到镜像仓库,或者直接交付给后端部署。

什么是 Dockerfile

​ Dockerfile是一种文本文件,用于定义Docker镜像的构建过程。它包含一系列命令和参数,用于指示Docker如何构建镜像,例如从哪个基础镜像开始、如何安装软件包、设置环境变量、暴露端口、复制文件等。通过编写Dockerfile,开发者可以将自己的应用程序和依赖项打包到一个镜像中,方便地在不同的环境中部署和运行。

什么是 .dockerignore

​ Dockerfile 中的 COPYADD 命令可以将本地文件或目录复制到 Docker 镜像中,包括不必要的文件或目录可能会导致镜像体积增大。为了减小镜像大小,可以使用 .dockerignore 文件来排除一些不需要的文件或目录。

.dockerignore 文件的语法和 .gitignore 文件类似,可以指定需要忽略的文件或目录,支持通配符等。

​ 举个例子,假设我们有一个 vite-app 的前端应用程序目录,该目录中包含 node_modules 目录、.git 目录、.vscode 目录等一些不必要的文件或目录,我们可以在该目录下创建一个 .dockerignore 文件,并将这些不需要的文件或目录列出来:

node_modules
.git
.vscode

​ 在使用 COPYADD 命令时,Docker 将会自动忽略 .dockerignore 文件中列出的文件或目录,从而减小镜像体积。

Dockerfile 示例

image-20230406100416072.png

​ Dockerfile 中的每个指令都会在构建过程中执行,最终构建出一个镜像,镜像中包含了运行容器所需的文件和配置。而 CMD ["http-server", "-p", "3000"] 则是指明了在容器启动时执行的默认命令。

打包命令

docker build -t <image name>:<tag> <path>

​ 在 Dockerfile 的工作目录下,执行 docker build -t v-admin-img .

image-20230406102249883.png

​ 可以看到 Dockerfile 的指令依次执行,最终打包出一个叫 v-admin-img 的镜像,可以通过docker images 或者 Docker Desktop查看.

image-20230406102629566.png

​ 尝试通过这个镜像运行容器

image-20230406102925377.png

image-20230406102955710.png

​ 依然可以通过 Docker Desktop 查看 容器状态

image-20230406103418468.png

多阶段优化

​ 仔细思考上述过程,一个工程化前端项目的部署流程:安装依赖 => 执行打包命令 => 得到资源文件 => 暴露端口 => 启动 server 。

​ 我们发现得到资源文件之后,环境中的 nodepnpm 都已经不再需要,只需要将文件放置于一个 server服务 环境中即可。

多阶段构建

​ Docker 基于不同镜像构建的阶段是指 Docker 多阶段构建(multi-stage builds),它的原理是利用 Dockerfile 中的多个 FROM 命令来创建多个阶段。每个阶段都可以基于不同的镜像,并且可以有不同的命令和指令,但是它们共享同一个构建上下文(build context),这使得可以在构建过程中从一个阶段到另一个阶段传递数据和文件。

​ 多阶段构建的主要优势在于它可以大大减小镜像的大小,因为在构建过程中可以去除不必要的文件和依赖项。在构建完整的应用程序镜像时,只需要将必要的文件和依赖项从前面的构建阶段拷贝到最终的镜像中即可。

​ 另外,多阶段构建还可以提高构建过程的效率,因为可以在同一个 Dockerfile 中指定多个阶段,从而避免了构建过程中不必要的重复操作。这样可以减少构建时间和资源消耗。

Dockerfile 改进

image-20230406140251677.png

# nginx.conf
server {
  listen 80;

  location / {
    root /usr/share/nginx/html;
    try_files $uri $uri/ /index.html;
  }
}

​ 采用多阶段构建,新的镜像丢掉了很多打包时(builder阶段)的依赖,大小有了质的变化。

image-20230406140733010.png

部署交付

文件交付

​ 在 Docker 中,镜像能以tar格式进行存储和传输。

​ .tar是一种文件格式,也称为tarball。它是一种归档文件格式,用于将多个文件打包到单个文件中,类似于zip文件,但通常不会进行压缩。tar文件通常用于在Unix和Linux系统中归档和分发文件和目录。

输出

​ 可以使用 docker save 命令将一个镜像保存成 tar 包,命令格式如下:

docker save -o <文件名>.tar <镜像名>:<标签>

-o--output 的缩写,用于指定输出的文件名或路径。

​ 执行后会在当前目录下生成一个 .tar 文件。

输入

​ 如果你有一个Docker镜像的tar文件,可以使用以下命令加载它:

docker load -i yourimage.tar

-i--input 的缩写,用于指定输入的文件名或路径。

​ 其中 yourimage.tar 是您要加载的Docker镜像的tar文件。执行此命令后,Docker将加载该镜像并将其添加到本地镜像存储库中。您可以使用docker images命令来查看已加载的镜像。

Docker 仓库

​ Docker 仓库是一个用于存储和管理 Docker 镜像的中央存储库。它允许用户上传、下载和管理 Docker 镜像,以及与他人共享 Docker 镜像。

​ Docker 仓库可以分为两种类型:

  1. 官方仓库(Official Repositories):由 Docker 官方维护的仓库,包含了一些常见的应用镜像,如 MySQL、Nginx、Redis 等。
  2. 用户仓库(User Repositories):由个人或组织创建的 Docker 仓库,用于存储自己构建的 Docker 镜像,可以公开或私有。

​ 通过 Docker 仓库,我们可以将自己构建的 Docker 镜像发布到公网或内网,供其他人或组织使用和管理。同时,Docker 仓库也是 Docker 集成、交付、管理的重要一环,为 DevOps (开发运维一体化) 提供了便利和支持。

私有仓库

如果你是在Docker Hub上拉取官方镜像,那么大概率不需要特殊的权限;但如果是私有仓库或第三方仓库,则需要检查访问权限。

以下是登录到一个私有仓库的示例:

docker login myregistry.example.com

该命令将提示您输入用户名和密码。如果认证成功,Docker CLI 将为您创建一个身份验证令牌,该令牌将在后续的 Docker CLI 命令中使用。

如果需要在登录时指定用户名和密码,可以使用以下命令:

docker login myregistry.example.com -u username -p password

该命令将使用指定的用户名和密码进行登录,并且令牌将在后续的 Docker CLI 命令中使用。

登录到第三方仓库也使用相同的命令。只需要将仓库地址改为相应的第三方仓库地址即可。

如果私有仓库或第三方仓库不支持 https, 可以添加 insecure-registries到配置 Docker 引擎, 使其能够与不安全的(即没有使用 HTTPS)Docker 注册表进行通信

image.png

拉取
docker pull myregistry.example.com/snapshot/lakereservoir-geoserver@sha256:2e45189f97717b309bc5f07f6d424fb486de5acb290c7240a79377c5678bbf06

这是一个从 Docker registry 中拉取镜像的命令,具体解析如下:

  • docker pull:拉取 Docker 镜像的命令。
  • myregistry.example.com:指定 Docker registry 的地址为 myregistry.example.com。
  • snapshot/lakereservoir-geoserver:指定需要拉取的镜像名称为 snapshot/lakereservoir-geoserver。
  • @sha256:2e45189f97717b309bc5f07f6d424fb486de5acb290c7240a79377c5678bbf06:通过指定镜像的摘要来拉取特定版本的镜像,该摘要为 sha256 加密算法生成的哈希值,用于唯一标识镜像内容。
推送
# docker tag <local image name> <registry>/<image name>:<tag>
docker tag myimage myregistry.example.com/myimage:v1.0.0

这个命令为 Docker 镜像打标签,将本地的 myimage 镜像打上 myregistry.example.com/myimage:v1.0.0 的标签,其中:

  • myimage 是本地镜像的名称
  • myregistry.example.com 是私有仓库的地址
  • myimage:v1.0.0 是打上的标签,表示该镜像的版本号为 v1.0.0

这个命令执行后,就可以将标签为 myregistry.example.com/myimage:v1.0.0 的本地镜像推送到私有仓库中了。

# docker push <registry>/<image name>:<tag>
docker push myregistry.example.com/myimage:v1.0.0

这个命令会将名为“myimage”的镜像并带有“v1.0.0”标签的镜像推送到私有仓库“myregistry.example.com”中。

  • <registry>:私有仓库的地址,例如“myregistry.example.com”。
  • <image name>:镜像的名称。
  • <tag>:镜像的标签,例如“v1.0.0”。如果没有指定标签,则默认使用“latest”标签。

总结

现在让我们回顾并尝试解答最开始的问题

  • Docker 是做什么的?

    • 一个 镜像 ,到处跑
  • Docker 的使用场景是什么?

    • 减少不同的环境中部署和运行的复杂度
    • 认知统一,开发运维一体化
  • Docker 对于前端来说,有哪些帮助?

    • 加强了对服务端的认知
    • 简化了部署过程
    • 提供了版本管理功能