Docker 初探

上文「一文了解什么是容器」介绍了容器的基本概念,本文接着介绍当前最流行的容器平台 Docker,并对其进行初步使用。

1 Docker 概览

Docker 是一个用于开发、发布和运行应用程序的开放容器平台。Docker 能够将应用程序与基础架构分离,以便快速交付软件。使用 Docker,我们可以像管理应用程序一样管理基础架构。通过利用 Docker 快速发布、测试与部署代码的方法,我们能够显著提升编写代码与在生产环境运行代码的效率。

1.1 Docker 平台的能力

Docker 提供在被称为容器的松散隔离环境中打包和运行应用程序的能力。容器是轻量的,其包含运行应用程序所需的一切,所以无须依赖主机当前安装的内容。Docker 的隔离性和安全性允许在同一主机同时运行多个容器。我们还可以在工作中共享容器,且能确保与我们共享容器的每个人获取的容器都能以相同方式工作。

Docker 提供工具和平台来管理容器的生命周期,包括:

  • 使用容器来开发应用程序及其支持组件;
  • 容器称为分发和测试应用程序的单元;
  • 准备就绪后,将应用程序作为容器部署到生成环境而不论生成环境是本地数据中心还是云环境还是混合云。

1.2 Docker 可用来做什么?

  • 应用程序的持续快速交付

    Docker 为开发人员提供了标准的应用程序运行环境,从而简化了软件开发周期。并且,容器非常适合持续集成和持续交付工作流程,为应用程序的持续快速交付提供了保障。

  • 响应式部署和扩展

    Docker 的可移植性和轻量性使得工作负载的动态管理(按照业务需要近乎实时的扩展或销毁应用程序)变得容易。

  • 同样的硬件上运行更多的工作负载

    Docker 轻量而快速,较虚拟机更经济高效,允许在同样的硬件资源上运行更多的工作负载。

1.3 Docker 架构

Docker 使用的是 C/S(客户端-服务器)架构。

Docker 架构

(Docker 架构 - 引用自 Docker Documentation

Docker 客户端和 Docker 守护程序(负责构建、运行和分发 Docker 容器)使用 UNIX 套接字或网络接口之上的 REST API 进行通信。Docker 客户端与 Docker 守护程序可位于同一系统,也可以位于不同的系统上(Docker 客户端可连接远程的 Docker 守护程序)。Docker Compose 也算 Docker 客户端的一种,其允许处理多个容器组成的应用程序。

  • Docker 守护程序

    Docker 守护程序(dockerd)负责监听 Docker API 请求并管理 Docker 对象(镜像、容器、网络和卷等)。Docker 守护程序还可以与其它守护程序进行通信来管理 Docker 服务。

  • Docker 客户端

    Docker 客户端(docker)是与 Docker 交互的主要方式。当使用诸如docker run之类的命令时,Docker 客户端会使用 Docker API 调用守护程序dockerd,守护程序dockerd会处理这些命令。Docker 客户端可与多个守护程序进行通信。

  • Docker 桌面

    Docker 桌面是一个「全家桶」安装包,包含了 Docker 客户端、Docker 守护程序、Docker Compose、Kubernetes 和凭证助手等功能。

  • Docker 镜像仓库

    Docker 镜像仓库用于存储 Docker 镜像。Docker Hub 是一个所有人都可以使用的镜像仓库,也是 Docker 默认的镜像存储仓库。

  • Docker 对象

    我们使用 Docker 时,主要是使用镜像、容器、网络、卷或插件等 Docker 对象,下面会简单介绍下镜像和容器这两个对象。

    • 镜像

      Docker 镜像是一个包含命令的创建 Docker 容器的只读模板。通常,一个镜像依赖另一个镜像并有一些额外的定制。

      创建自己的 Docker 镜像时,使用Dockerfile来定义构建与运行镜像的所需步骤。Dockerfile中的每条命令都会在镜像中创建一个层,当修改Dockerfile并重新构建镜像时,只有变化的层会被重新构建,这也是容器镜像比其它虚拟技术更轻量快速的原因。

    • 容器

      容器是镜像的运行实例。我们可以使用 Docker 客户端或调用 Docker API 创建、启动、停止、移动或删除容器,可以为容器连接网络,给容器添加存储,甚至可以根据容器当前状态创建一个新镜像。

      默认情况下,容器与其它容器及主机是严格隔离的。当然,容器的网络、存储等与其它容器及主机的隔离程度是可以控制的。

      补充说明:Docker 是使用 Go 语言编写的,且利用了 Linux 内核提供的特性。Docker 使用命名空间技术来提供容器的空间隔离。

2 Docker 安装

最直接快速安装 Docker 的方法就是安装 Docker 桌面。本文使用的操作系统为 MacOS,直接从「Docker Desktop for Mac」下载最新的版本,双击运行后「Accept」即可。

3 Docker 初步使用

3.1 容器化应用程序

下面使用一个Node.js编写的「待办列表」示例应用程序来演示 Docker 的使用。

开始前,先将代码克隆下来:

git clone https://github.com/docker/getting-started.git

然后,可以看到getting-started/app文件夹下有两个子文件夹srcspec,以及一个package.json文件。

getting-started
├─ app
│   ├─ src/
|   ├─ spec/
│   └─ package.json
└─ ...

下面,在getting-started/app文件夹下新建一个Dockerfile文件,并为其添加如下内容:

# syntax=docker/dockerfile:1

FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
EXPOSE 3000

然后,在getting-started/app文件夹下执行docker build命令来构建镜像:

# -t 表示给镜像起一个名字
# . 表示在当前文件夹寻找 Dockerfile
docker build -t getting-started .

镜像构建完成后,使用docker run命令来启动容器:

# -d 表示以后台方式运行
# -p 表示使用主机的 3000 端口映射容器的 3000 端口
# getting-started 即是要运行的镜像名
docker run -dp 3000:3000 getting-started

这样,浏览器访问http://localhost:3000即可以看到应用程序了:

待办列表示例应用程序

此外,还可以使用docker ps命令来查看容器状态,使用docker stop命令来停止容器,以及对停止的容器使用docker rm来进行移除。

3.2 镜像推送与分享

下面,尝试将镜像推送到「Docker Hub」。

开始前,首先需要注册一个 Docker Hub 账号,我的账号为 olzhy。

接着,使用docker login命令登录到 Docker Hub:

docker login

然后,使用docker tag命令将getting-started镜像重命名:

docker tag getting-started olzhy/getting-started

最后,使用docker push命令将镜像推送至 Docker Hub:

docker push olzhy/getting-started

这样,任何人即可以在安装了 Docker 的机器上使用我们刚刚推送的镜像了:

docker run -dp 3000:3000 olzhy/getting-started

3.3 数据库持久化

目前的这个「待办列表」示例应用程序重启后,数据会丢失。这是因为未对数据库进行持久化,下面看一下如何持久化数据库。

卷(Volume)提供了将容器的特定文件系统路径映射到主机的功能。

「待办列表」示例应用程序使用的是 SQLite 数据库,其数据存储在文件/etc/todos/todo.db中。

下面,使用docker volume create命令创建一个卷:

docker volume create todo-db

然后,指定挂载的卷,并启动容器:

docker run -dp 3000:3000 --mount type=volume,src=todo-db,target=/etc/todos getting-started

启动完成后,增加一些数据。这时,停止并移除上述容器后,再次使用如上命令启动新的容器时,仍可以看到之前添加的数据。

最后,使用docker volume inspect命令看一下数据到底存到了哪里:

docker volume inspect todo-db

[
    {
        "CreatedAt": "2023-05-21T02:27:07Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
        "Name": "todo-db",
        "Options": null,
        "Scope": "local"
    }
]

挂载点(Mountpoint)显示了数据在主机的具体位置。

除了使用卷外,还可以使用绑定挂载(Bind Mounts)来将主机的任一文件或文件夹挂载到容器。

使用方式与卷类似,下面是使用 Bind Mounts 挂载方式运行容器的命令:

docker run -dp 3000:3000 --mount type=bind,src=/tmp/todos,target=/etc/todos getting-started

# 亦可以直接简化为 -v 方式
docker run -dp 3000:3000 -v /tmp/todos:/etc/todos getting-started

3.4 多容器应用

下面,新建一个 MySQL 数据库容器,然后尝试用「待办列表」容器连接这个数据库。

两个容器需要使用网络进行通信,首先需要创建网络:

docker network create todo-app

接着,运行 MySQL 容器:

# 可以看到,挂载的时候,未创建卷 todo-mysql-data,这个时候 Docker 会自动帮我们创建
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:8.0

使用如下命令进入容器,尝试连接数据库并执行数据库命令:

docker exec -it <mysql-container-id> mysql -u root -p
mysql> SHOW DATABASES;

+--------------------+
 | Database           |
 +--------------------+
 | information_schema |
 | mysql              |
 | performance_schema |
 | sys                |
 | todos              |
 +--------------------+
 5 rows in set (0.00 sec)

可以看到,数据库todos已被创建。

下面,进入getting-started/app文件夹,使用如下命令来启动「待办列表」容器:

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:18-alpine \
   sh -c "yarn install && yarn run dev"

访问应用程序,并增加一些条目。

这时,查看数据库时,发现表里边已经写入了数据:

docker exec -it <mysql-container-id> mysql -p todos
mysql> select * from todo_items;
 +--------------------------------------+--------------------+-----------+
 | id                                   | name               | completed |
 +--------------------------------------+--------------------+-----------+
 | c906ff08-60e6-44e6-8f49-ed56a0853e85 | Do amazing things! |         0 |
 | 2912a79e-8486-4bc3-a4c5-460793a575ab | Be awesome!        |         0 |
 +--------------------------------------+--------------------+-----------+

3.5 使用 Docker Compose

上面,启动多个容器时,需要考虑新建网络、启动容器,暴露端口和指定环境变量等一系列步骤。而如果使用 Docker Compose 的话,就会变得很简单。

Docker Compose 是一个定义多容器应用程序的工具。

下面,在getting-started/app文件夹下创建一个名为docker-compose.yml的文件。

然后,将如下内容填充到该文件中:

services:
  app:
    image: node:18-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:8.0
    volumes:
      - todo-mysql-data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: todos

volumes:
  todo-mysql-data:

可以看到,该 Compose 文件配置了应用程序和 MySQL 两个服务,配置参数非常的简单明了。

接着,使用如下命令启动容器:

docker compose up -d

使用如下命令查看日志:

docker compose logs -f

测试完成后,可使用如下命令移除容器:

# 若要将 Volume 一并移除,需要加 --volumes 标记
docker compose down

3.6 镜像构建最佳实践

利用镜像分层缓存加快构建速度

基于Dockerfile进行镜像构建时,一旦某一层发生变化,后面的步骤都需要重新构建。

看一下前面构建「待办列表」应用程序的Dockerfile文件:

# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

其存在几个问题:

  • COPY 时,未指定应当忽略的文件夹,node_modules会被拷贝进去;
  • 任何文件有修改时,都需要重新进行yarn install

下面,在当前文件夹下新建一个.dockerignore文件,并添加如下内容:

node_modules

表示 COPY 时,忽略node_modules文件夹。

接着,对Dockerfile文件进行一下改造,改造后的内容如下:

# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --production
COPY . .
CMD ["node", "src/index.js"]

改造的思路是:yarn install主要依赖package.json文件,所以将这两步放到一块,这样只要不改package.json这个文件,就不用重新进行yarn install

经过改造后,较之前会大大节省镜像的构建时间。

利用多阶段构建减小镜像体积

多阶段构建可以将构建时依赖项与运行时依赖项分开,并且可以通过仅提供运行所需的内容来减小镜像的体积。

下面用两个具体的例子来说明如何进行多阶段构建。

一个是 Maven/Tomcat 应用程序的例子:当构建一个使用 Maven 管理的 Java 应用程序时,JDK 和 Maven 是必需的;而在运行时,JDK 和 Maven 却不是必需的。这时就可以使用多阶段构建来帮忙了。

针对该应用程序,使用多阶段构建的Dockerfile文件内容可以是如下这样:

# 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 源码编译为一个war包;第二个阶段准备了一个 Tomcat 环境,然后将第一个阶段生成的war包拷贝到对应位置。最终的镜像只有 Tomcat 这个阶段的部分,省去了很多没必要的依赖。

另一个是 React 应用程序的例子:当构建 React 应用程序时,需要依赖Node.js环境来编译 JSX 源码文件和 SASS 样式文件;而在运行时,可以不依赖Node.js环境,直接使用 Nginx 容器 Serve 这些静态文件即可。

针对该应用程序,使用多阶段构建的Dockerfile文件内容可以是如下这样:

# syntax=docker/dockerfile:1
FROM node:18 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

可以看到,第一个阶段(build)基于Node.js环境将 JSX 源码文件和 SASS 样式文件编译为 HTML、JS 和 CSS 静态文件;第二个阶段仅需要一个 Nginx 环境,然后将第一个阶段生成的静态文件拷贝到对应目录。

综上,本文完成了对 Docker 的初探。阅读完本文,我们对 Docker 是什么、Docker 能做什么、Docker 的架构是什么样的以及 Docker 怎么使用都有了一个基本的了解。

参考资料

[1] Get started | Docker Documentation - docs.docker.com

评论

正在加载评论......