Docker 初体验

经常在各个地方看到 Docker,关于什么是 Docker,官方文档已经给了专业详尽的解释,个人粗浅的理解是它不仅将应用程式,还将应用程式所需要的环境和各种依赖一起打包成了一个盒子,当我们使用这个应用程式时,直接“打开”盒子就能用,不需要本机电脑再安装配置什么,因为需要的东西盒子里已经都有了。

那这样有什么用呢?对于个人开发者来说,假如设备同时运行着几个项目,而这些项目所需要的环境各有不同,如果不用 Docker,可能就会相互冲突了。

而对于公共项目来说,假如一个用户数量庞大的公共项目,每个人的设备各有不同,有的安装了 A 的这个版本,有的安装了 B 的那个版本,有的只安装了一部分,怎么保证这个项目能在所有设备上正常平稳地运行呢?直接连同所用的环境打包成 Docker 是个不错的选择。

由于 Docker 能够轻松地封装应用程序和其依赖项,所以大大方便并简化了应用程序的部署和使用。

◇ Docker 的用途

Docker 的主要用途,目前有 3 大类:

  1. 提供一次性的环境。比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。
  2. 提供弹性的云服务。因为 Docker 容器可以随开随关,很适合动态扩容和缩容。
  3. 组建微服务架构。通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。

◇ Windows 精简版初体验

官方推荐 Windows 安装 Docker Desktop,因为安装包有点大,旧电脑磁盘空间有些告急,所以先通过 Scoop 下载体验一下。

注意: 通过此方法下载的程序只包含用于 Windows 容器的 Docker CLI 和 Docker Engine。仅支持运行 Windows 容器,可以通过下载镜像连接到现有的 Linux 容器。建议直接采用官方的推荐安装 Docker Desktop,功能更全,也方便添加拓展等。

▷ 安装

1
scoop install docker    # 安装 docker

▷ 注册服务

安装完毕后,输入 dockerd --register-service 命令,注册服务。发现访问被拒绝,这是因为 Docker 需要用户具有管理员权限。

1
2
$ dockerd --register-service
Access is denied.

重新用 sudo 命令输入,没有返回报错,说明已经成功了。

1
sudo dockerd --register-service

dockerd 是 Docker 守护程序,是在后台运行的服务,负责管理 Docker 容器。当在 Windows 系统上运行该命令时,会将 Docker 守护程序注册为一个 Windows 服务。

将 Docker 守护程序注册为服务意味着在 Windows 系统启动时,Docker 将自动启动,而不必每次登录或系统重新启动时手动启动它。

💡 如果需要取消注册服务,可以通过以下命令来完成:

1
sudo dockerd --unregister-service

▷ 启动服务

为了避免每次命令都输入 sudo,可以以管理员身份开启窗口。(或者将当前用户加入 Docker 用户组,参见:将用户加入 Docker 用户组)

接下来输入 start-service docker 命令启动 Docker 引擎,但发现启动服务失败了。

1
2
<root>$ start-service docker
Start-Service: Failed to start service 'Docker Engine (docker)'.

使用快捷键 win+x,选择并打开 “事件查看器” (Event Viewer)。

找到关于 Docker 的错误信息,显示:

fatal: failed to start daemon: failed to load vmcompute.dll, ensure that the Containers feature is installed

如图所示:
Event Viewer

上面的信息显示当 Windows 尝试启动 Docker 守护程序 (Docker daemon) 时,无法加载 “vmcompute.dll”。这通常是由于缺少或损坏了 Windows 容器所需的组件,或者没有正确安装 Windows 容器功能引起的。

打开 “Windows 功能”,发现 “Windows 容器” 确实没有开启,所以需要勾选该选项,点击确认后电脑需要重启。
Windows 功能

重启之后再运行上面的命令,没有返回错误信息,表明启用成功了。

1
<root>$ start-service docker

💡 如果需要停止服务,可以通过以下命令来完成:

1
<root>$ Stop-Service docker

▷ 测试

安装完成后我们可以通过运行 hello-world 镜像来验证 Docker 是否已正确安装:

1
<root>$ docker run hello-world:nanoserver

nanoserver 是指示使用 Windows Nano Server 基础镜像的版本。Windows Nano Server 是一种轻量级 Windows Server 镜像,适用于 Windows 容器。

结果又返回了错误信息:

1
docker: Error response from daemon: hcs::CreateComputeSystem 158a8fd0bc9555ebd7a32eafc3b8ad4df6047fe2a5be95e511c468835eaa205a: The request is not supported.

查询得知 Windows 容器需要 Hyper-V 技术来提供虚拟化支持,打开 “Windows 功能”,发现果然没有开启,于是勾选该选项,勾选后需要重启电脑。
Hyper-V

重启之后再运行上面的命令,发现终于可以成功运行了。显示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<root>$ docker run hello-world:nanoserver

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(windows-amd64, nanoserver-1809)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run a Windows Server container with:
PS C:\> docker run -it mcr.microsoft.com/windows/servercore:1809 powershell

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/

▷ 将用户加入 Docker 用户组

为了避免每次命令都使用管理员身份,可以把用户加入 Docker 用户组。

- 创建组并添加用户

先创建一个用户组,组名可以自定义,我这里直接命名为 “docker”。管理员身份运行以下命令:

1
2
3
4
<root>$ New-LocalGroup -Name "docker" -Description "Docker 用户组"

# 或是以下命令
<root>$ net localgroup docker /add

然后将当前用户加入这个组:

1
2
3
4
<root>$ Add-LocalGroupMember -Group "docker" -Member "<your_username>"

# 或是以下命令
<root>$ net localgroup docker <your_username> /add

💡 如果想要移除用户组或组里的用户,可以使用以下命令:

1
2
3
4
5
6
7
8
9
10
# 移除某个用户组
<root>$ Remove-LocalGroup -Name "<group_name>"
# 移除组里的某个用户
<root>$ Remove-LocalGroupMember -Group "<group_name>" -Member "<user_name>"

# 或是以下命令
# 移除某个用户组
<root>$ net localgroup <group_name> /delete
# 移除组里的某个用户
<root>$ net localgroup <group_name> <user_name> /delete

- 配置用户组的 Docker 访问权限

Docker Desktop 和 Linux 平台似乎已经配置好了,直接创建特定组 (Docker Desktop for Windows 名为:docker-users;Linux 平台名为:docker) 并添加用户即可。

但是这个精简版并没有啊!刚才创建的组 Docker 其实并不认识。而网上全是关于 Docker Desktop 和 Linux 的教程,最后本人好不容易在 “Windows 容器” 的官方文档中找到了配置的方法。

首先创建配置文件 C:\ProgramData\Docker\config\daemon.json,然后打开文件,输入以下内容,设置 Docker 安全组:

1
2
3
{
"group" : "docker" // 改成上一步创建的组的名称
}

设置好后,重启 Docker 服务:

1
<root>$ Restart-Service docker

然后再去普通用户窗口输入 docker 命令,发现已经不报错了:

⚠ 如果还是报错,显示没有权限,重新登录用户或者重启一下设备,用来刷新用户的组员资格,使其生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ docker version
Client:
Version: 24.0.6
API version: 1.43
Go version: go1.20.7
Git commit: ed223bc
Built: Mon Sep 4 12:32:48 2023
OS/Arch: windows/amd64
Context: default

Server: Docker Engine - Community
Engine:
Version: 24.0.6
API version: 1.43 (minimum version 1.24)
Go version: go1.20.7
Git commit: 1a79695
Built: Mon Sep 4 12:31:39 2023
OS/Arch: windows/amd64
Experimental: false

▷ 其它配置

Windows 中 imagecontainer 等文件默认保存在 c:\programdata\docker,因为本人的旧电脑 C 盘已爆满,所以需要修改一下保存路径。

打开配置文件 C:\ProgramData\Docker\config\daemon.json,新增内容:

1
2
3
{   
"data-root": "d:\\docker" //修改为需要保存的路径
}

注意每个配置项末尾需要以 , 分割,最后一项不需要加。

设置好后,重启 Docker 服务:

1
sudo Restart-Service docker

▷ 卸载方法

先记录一下卸载大致流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 从 Docker Swarm 集群中强制退出当前节点
docker swarm leave --force
# 停止所有正在运行的 Docker 容器
docker ps --quiet | ForEach-Object {docker stop $_}
# 清理 Docker 环境中的未使用资源,包括容器、镜像、网络和卷
docker system prune --volumes --all
# 停止 Docker 服务
sudo Stop-Service docker
# 注销 Docker 服务
sudo dockerd --unregister-service
# 卸载 Docker
scoop uninstall docker
# 删除 Docker 的默认网络
Get-HNSNetwork | Remove-HNSNetwork
# 从系统中删除 Docker 的程序数据
Remove-Item "C:\ProgramData\Docker" -Recurse
# 删除自定义路径的数据
Remove-Item "D:\Docker" -Recurse
# 移除设置的用户组
sudo Remove-LocalGroup -Name "docker"
# 手动关闭 Hyper-V 和 Windows 容器
手动关闭 Hyper-V 和 Windows 容器
# 重启系统
sudo Restart-Computer -Force

◇ 命令相关

1
2
3
4
5
6
7
8
9
10
11
# 查看版本信息
docker version
# 查看信息
docker info

# 列出本机的所有 image 文件
docker image ls
# 列出本机正在运行的容器
docker container ls
# 列出本机所有容器,包括终止运行的容器
docker container ls --all
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 将 image 文件从仓库抓取到本地
docker image pull library/hello-world
# library 为官方默认组,可省略为
docker image pull hello-world

# 运行指定 image,生成一个正在运行的容器实例
docker container run hello-world
# 可缩写为
docker run hello-world
# 运行特定版本的容器
docker run hello-world:nanoserver

# 安装并运行 Ubuntu 的 image
docker container run -it ubuntu bash
# 安全终止运行中的容器
docker container stop <container_ID>
# 强制终止运行中的容器
docker container kill <container_ID>

# 启动已经生成、已经停止运行的容器文件
docker container start <container_ID>

# 查看容器的输出,即容器里面 Shell 的标准输出
docker container logs <container_ID>
# 进入正在运行的 Docker 容器内执行命令
docker container exec -it <container_ID> /bin/bash
# 从正在运行的容器里面拷贝文件到本机当前目录
docker container cp <container_ID>:</path/to/file> .
1
2
3
4
5
6
# 运行容器并在容器终止运行后自动删除容器文件
docker container run --rm <image_name>
# 删除 image 文件
docker image rm <image_name>
# 删除容器文件
docker container rm <container_ID>
1
2
3
4
5
6
# 登录 Docker Hub
docker login
# 为本地的 image 标注名称和版本
docker image tag <image_name> <user_name>/<repository>:<tag>
# 发布 image 文件
docker image push <user_name>/<repository>:<tag>

◇ 项目实例

▷ sub-web

先前使用过的 sub-web 项目正好也有 Docker 版,之前是下载源码使用的 (参见:链接),现在来看看 Docker 部署的方法。

- 部署

> 直接部署

可以直接下载项目官方的 Docker 镜像进行部署:

1
docker run -d -p 58080:80 --restart always --name subweb careywong/subweb:latest

一些参数解释:

  • -d:让容器在后台运行,启动后会立即恢复到本机命令提示符。
  • -p 58080:80:将主机的 58080 端口映射到容器的 80 端口,即可以通过主机 http://localhost:58080 访问运行在容器中的 web 服务。
  • --restart always:始终自动重启。
  • --name subweb:自定义容器名称为“subweb”。
  • careywong/subweb:latest:拉取镜像的仓库、名称和版本。
> 自定义部署

如果有自定义需求,需要下载源码修改,然后自行打包生成镜像再运行:
(但是不推荐这种方法,不方便维护,推荐使用后面自动构建的方法,参见:链接)

1
2
docker build -t subweb-local:latest .
docker run -d -p 58080:80 --restart always --name subweb subweb-local:latest

一些参数解释:

  • docker build:构建一个新的 Docker 镜像。
  • -t subweb-local:latest:指定构建的镜像的名称和标签。
  • .:构建镜像所需的 Dockerfile 文件的目录,这里表示当前目录。

- Dockerfile

上面提到的 “Dockerfile” 是一个文本文件,包含构建 Docker 镜像所需的指令和配置。本项目的 Dockerfile 文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
# ---- Dependencies ----
FROM node:16-alpine AS build
WORKDIR /app
COPY . .
RUN yarn install
RUN yarn build

FROM nginx:1.24-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD [ "nginx", "-g", "daemon off;" ]

上面的文件采用了 Docker 多阶段构建 (multi-stage build) 的方法,这种方法可以用来优化镜像的大小和性能。

在上面的例子中,第一个阶段(build)用于构建应用程序,第二个阶段用于构建 Nginx 服务器的镜像。

下面详细解释一下它的含义:

1
FROM node:16-alpine AS build

FROM 指定了第一个构建阶段的基础镜像。它使用了 Node.js 16Alpine Linux 版本,并将其命名为 “build”,以便在后续引用中使用。

Alpine Linux 是一种轻量级的 Linux 发行版,专注于提供最小的系统资源消耗,同时具备强大的安全性和可定制性。大多数 Docker 项目也基于此版本。

1
2
WORKDIR /app
COPY . .

设置容器工作目录为 /app,然后将当前目录中的所有文件复制到容器的工作目录中。

💡 如果有需要排除的文件,可以在当前目录新增 .dockerignore 文件,然后填写需要排除的路径,类似 Git 中的 .gitignore 文件。

1
2
RUN yarn install
RUN yarn build

使用 Yarn 安装项目的依赖包,然后进行构建,从之前搭建的经验可以得知,构建生成的内容将会在 /dist 目录。

1
FROM nginx:1.24-alpine

此时来到了镜像构建的第二阶段,FROM 指定了第二个构建阶段的基础镜像,即 Nginx 1.24Alpine Linux 版本。

Nginx 是一个开源的高性能、轻量级的 Web 服务器和反向代理服务器软件,能够将客户端的请求转发到多个后端服务器上,可用于托管网站和 Web 应用程序。

1
COPY --from=build /app/dist /usr/share/nginx/html

从第一个构建阶段 “build” 阶段复制生成文件到 Nginx 容器中的 /usr/share/nginx/html 目录,这将配置 Nginx 以显示生成的前端页面。

1
2
EXPOSE 80
CMD [ "nginx", "-g", "daemon off;" ]

EXPOSE 指定容器侦听的端口,这里是 80。
然后通过 CMD 指定容器启动时要运行的命令:nginx -g daemon off;
其中 daemon off;-g 选项后面的参数,表示让 Nginx 以前台进程运行,以确保容器保持运行状态。

在 Docker 容器中运行服务时,通常推荐让服务以前台进程运行。这是因为 Docker 的最佳实践是一个容器只运行一个进程。当这个进程退出时,容器也就结束了。

如果 Nginx 以后台进程 (即守护进程) 运行,那么在 Nginx 启动后,启动命令就立即结束了,Docker 会认为服务已经结束,于是就会关闭容器。

因此,我们需要通过 daemon off; 指令让 Nginx 以前台进程运行,这样 Docker 容器就会一直运行,直到 Nginx 服务结束。

普通情况下,Nginx 以后台守护进程的方式运行,但在 Docker 容器中,以非守护进程模式运行更为常见,以便容器保持运行状态。

- 自动构建镜像

如果有自定义需求,修改源码本地构建肯定是不方便的,我们可以利用 GitHub Actions 来实现自动构建 Docker 镜像并实时发布到 Docker Hub,这样部署的时候直接拉取我们在 Docker Hub 自定义的镜像即可。

本项目的 Docker 构建工作流文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
name: Build and Push Multi-Arch Docker Image

on:
push:
branches:
- master

jobs:
build-and-push:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@master

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: careywong/subweb:latest

这个工作流文件主要分为 4 个步骤:

首先通过官方的 actions/checkout 从当前的GitHub仓库中检出代码。

然后通过 Docker 官方的 docker/setup-buildx-action 来设置 Docker Buildx,一个用于构建多架构的 Docker 镜像的工具。

之后需要登录 Docker Hub,登录所需的用户名和密码通过 GitHub Secrets 提供,需要在仓库的 Settings > Secrets and variables > Actions > new repository secret 按照上面的名称新建 2 个变量,一个是 Docker 账号的用户名,一个在 Docker 账户生成的 token,用于登录。如下图所示:
Repository secrets

最后通过 docker/build-push-action 来构建和推送 Docker 镜像:

  • context: .:表示构建上下文是当前目录,即代码库的根目录。
  • platforms: linux/amd64,linux/arm64:指定要构建的目标平台,这里构建了两个架构的镜像,分别是 x86_64 (amd64) 和 ARM64。
  • push: true:表示构建后将镜像推送到 Docker Hub。
  • tags: careywong/subweb:latest:指定了要构建的镜像的名称和标签。这里需要修改为自己的名称,如 <user_name>/sub-web:latest

修改完成后将代码推送至远程仓库,Action 会自动运作,将构建好的镜像推送至自己的 Docker Hub:
Docker Hub

之后我们便可在任意设备下载自己的镜像进行部署:

1
docker run -d -p 58080:80 --restart always --name subweb <user_name>/sub-web:latest

◇ 参考内容

  1. Failed to Start Docker Service on Windows 10 AE. https://dscottraynsford.wordpress.com/2016/08/04/failed-to-docker-service-on-windows-10-ae/
  2. Install Docker Engine from binaries. https://docs.docker.com/engine/install/binaries/
  3. Error response from daemon: hcsshim::CreateComputeSystem. https://github.com/microsoft/navcontainerhelper/issues/811
  4. Running any Windows container returns: docker: Error response from daemon: hcsshim::CreateComputeSystem …: The request is not supported. #227. https://github.com/microsoft/Windows-Containers/issues/227
  5. Docker for Windows - Access Denied #868. https://github.com/docker/for-win/issues/868
  6. Windows 上的 Docker 引擎. https://learn.microsoft.com/zh-cn/virtualization/windowscontainers/manage-docker/configure-docker-daemon#configure-docker-on-the-docker-service
  7. Docker 入门教程. https://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html