跳转至

2.1 Docker的原理和基本使用

学习目标

  • 了解虚拟机和Docker的区别
  • 掌握Docker的原理
  • 掌握Docker的基本命令使用
  • 掌握Docker的镜像构建

一、环境配置的难题

软件开发过程中,环境配置的一致性是一个普遍存在的挑战。由于目标计算机的运行环境存在差异,开发者难以确保软件能在各种目标机器上正常运行。

成功运行软件需要满足两个关键条件:正确的操作系统设置,以及准确的库和组件安装。以 Python 应用为例,目标机器不仅需要安装 Python 引擎,还必须正确安装所有依赖包,并可能需要进行特定的环境变量配置。

当某些遗留模块与当前环境不兼容时,问题会变得尤为复杂。开发者常说的“在我的机器上可以运行”(It works on my machine),其隐含意义是软件在其他环境中很可能无法正常工作。环境配置过程繁琐复杂,每次更换机器都需要重复进行,耗时费力。

很多人想到,能不能从根本上解决问题,软件可以带环境安装?也就是说,安装的时候,把原始环境一模一样地复制过来。

二、虚拟机

对于“它在我机器上能跑” 这个问题,解决思路就是把软件运行所需的操作系统片段依赖库配置文件全部打包成一个“独立盒子”(容器/镜像),这个盒子能在任何支持该技术的机器上“开箱即用”,彻底避免“在我这能跑”的尴尬。 最早的解决方案,就是虚拟机。

1 什么是虚拟机

虚拟机(virtual machine)就是带环境安装的一种解决方案。它可以在一种操作系统里面运行另一种操作系统,比如在 Windows 系统里面运行 Linux 系统。应用程序对此毫无感知,因为虚拟机看上去跟真实系统一模一样,而对于底层系统来说,虚拟机就是一个普通文件,不需要了就删掉,对其他部分毫无影响。

image-20251109180140389

接下来以一个形象的例子来对比物理机和虚拟机的区别:

物理机:

  • 一个机器里面一个操作系统 - 独立地基,独立花园

img

虚拟机:

  • 一个机器里面多个操作系统 - 共享地基, 共享花园。 但是内部空间是互相隔离的

image-20250427181809159

简单来讲,虚拟机通过把物理机的资源进行分割,安装多个操作系统,多个操作系统互相独立,并独享资源。

2 虚拟机案例

在Windows系统中,有一个比较出名的虚拟机软件:vmware,它的管理页面如下:

img

如上图,我们可以看到当前我们在一个windows 10系统中运行了一个vmware软件, 这个vmware软件中又运行着windows xp和windows 8.1两台虚拟机,这里有个几点需要注意:

  1. vmware运行所在的windows系统是物理机
  2. windows xp没有运行,在这种状态下,它只是一个“镜像文件”。这个镜像文件只有使用vmware运行的时候,才会转变成一个运行状态的“虚拟机
  3. windows 8.1虚拟机处在运行状态下, 相当于是跑在windows 10中的一个特殊的软件,运行时,它会占用大量的资源(占用的数量和我们在vmware分配给它的资源量一致)。 而我们进入windows 8.1中使用它的时候, 使用它和使用物理机安装的windows 8.1几乎没有区别。

vmware属于个人玩家使用的虚拟化软件,且仅对windows环境友好。如果是在服务器进行虚拟化,一般会使用exsi等系统管理物理机,exsi不仅是一个虚拟机管理程序,它本身也是一个操作系统,可以节省资源,并对虚拟化支持更好。

3 虚拟机的缺点

虽然用户可以通过虚拟机还原软件的原始环境。但是,这个方案有几个缺点:

  1. 资源占用多: 虚拟机会独占一部分内存和硬盘空间。它运行的时候,其他程序就不能使用这些资源了。哪怕虚拟机里面的应用程序,真正使用的内存只有 1MB,虚拟机依然需要几百 MB 的内存才能运行。

  2. 冗余步骤多:虚拟机是完整的操作系统,一些系统级别的操作步骤,往往无法跳过,比如用户登录。

  3. 启动慢:启动操作系统需要多久,启动虚拟机就需要多久。可能要等几分钟,应用程序才能真正运行。

4 Linux 容器

由于虚拟机存在这些缺点,Linux 发展出了另一种虚拟化技术:Linux 容器(Linux Containers,缩写为 LXC)。

Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离。或者说,在正常进程的外面套了一个“壳”。对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。

由于容器是进程级别的,相比虚拟机有很多优势:

  1. 启动快:容器里面的应用,直接就是底层系统的一个进程,而不是虚拟机内部的进程。所以,启动容器相当于启动本机的一个进程,而不是启动一个操作系统,速度就快很多。

  2. 资源占用少:容器只占用需要的资源,不占用那些没有用到的资源;虚拟机由于是完整的操作系统,不可避免要占用所有资源。另外,多个容器可以共享资源,虚拟机都是独享资源。

  3. 体积小:容器只要包含用到的组件即可,而虚拟机是整个操作系统的打包,所以容器文件比虚拟机文件要小很多。

如下图,对比物理机的“独栋别墅”和虚拟机的“楼房”,docker更像是胶囊式公寓:

PixPin_2025-11-25_02-09-23

总之,容器有点像轻量级的虚拟机,能够提供虚拟化的环境,但是成本开销小得多。

5 拓展: 什么是虚拟化

在计算机中,虚拟化(英语:Virtualization)是一种资源管理技术,是将计算机的各种实体资源,如:服务器、网络、内存、存储等等,予以抽象、转换后呈现出来,打破实体结构间的不可切割的障碍,使用户可以比原来的组态更好的方式来应用这些资源,这些资源的核心虚拟部分是不受现有资源的架设方式,低于或者物理组态所限制,一般所指的虚拟化资源包括计算能力和资料存储。

在实际的生产过程中,虚拟化技术主要是用来解决高性能的物理硬件产能过剩和老的硬件产能过低的重用重组,透明化底层物理硬件,从而最大化的利用物理硬件,对资源充分利用。

虚拟化技术种类很多,例如:软件虚拟化、硬件虚拟化、内存虚拟化、网络虚拟化(vip),桌面虚拟化、服务虚拟化、虚拟机等等。

虚拟化简单讲,就是把一台物理计算机虚拟成多台逻辑计算机,每个逻辑计算机里面可以运行不同的操作系统,相互不受影响,这样就可以充分利用硬件资源

三、Docker是什么

1 什么是Docker

我们前面提到了Linux容器这个概念:Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前最流行的 Linux 容器解决方案。

无标题

Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。

img

总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。

Docker 是一个开源的商业产品,有两个版本:社区版(Community Edition,缩写为 CE)和企业版(Enterprise Edition,缩写为 EE)。企业版包含了一些收费服务,个人开发者一般用不到。下面的介绍都针对社区版。

2 Docker与虚拟机的对比

什么是虚拟机

虚拟机是一个计算机系统的仿真,简单来说,虚拟机可以实现在一台物理计算机上模拟多台计算机运行任务。 操作系统和应用共享一台或多台主机(集群)的硬件资源,每台VM有自己的OS,硬件资源是虚拟化的。管理程序(hypervisor)负责创建和运行VM,它连接了硬件资源和虚拟机,完成server的虚拟化。 由于虚拟化技术和云服务的出现,IT部门通过部署VM可以可减少cost提高效率。

image-20250427181922388

VMs也消耗大量系统资源,每个VM不仅运行一个OS的完整copy并且需要所有硬件的虚拟化copy,这消耗大量RAM和CPU。相比单独计算机,VM是比较经济的,但对于一些应用VM是过度浪费的,需要容器。

什么是容器

容器是将操作系统虚拟化,这与VM虚拟化一个完整的计算机有所不同。 容器是在操作系统之上,每个容器共享OS内核,执行文件和库等。共享的组件是只读的,通过共享OS资源能够减少复现OS的代码,意味着一台server仅安装一个OS可以运行多个任务。容器是非常轻量的,仅仅MB水平并且几秒即可启动。相比容器,VM需要几分钟启动,并且大小也大很多。 与VM相比,容器仅需OS、支撑程序和库文件便可运行应用,这意味你可以在同一个server上相比VM运行2-3倍多的应用,并且,容器能帮助创建一个可移植的,一致的开发测试部署环境。

image-20250427181909472

小结:

特性 虚拟机 容器
隔离级别 操作系统级 进程级
隔离策略 运行于Hypervisor上 直接运行在宿主机内核中
系统资源 5-15% 0-5%
启动速度 慢,分钟级 快,秒级
占用磁盘空间 非常大,GB-TB级 小,KB-MB甚至KB级
并发性 一台宿主机十几个,最多几十个 上百个,甚至上百上千个
高可用策略 备份、容灾、迁移 弹性、负载、动态

结论:

与传统的虚拟化相比,Docker优势体现在启动速度快,占用体积小。

四、Docker核心组件

1 Docker服务端和客户端

Docker 是一个客户端-服务端(C/S)架构程序,Docker客户端只需要向Docker服务端或者守护进程发出请求,服务端或者守护进程完成所有工作返回结果,Docker提供了一个命令行工具Docker以及一整套的Restful API,可以在同一台宿主机器上运行Docker守护进程或者客户端,也可以从本地的Docker客户端连接到运行在另一台宿主机上的远程Docker守护进程

docker引擎是一个c/s结构的应用,主要组件见下图:

Docker使用C/S架构,Client 通过接口与Server进程通信实现容器的构建,运行和发布。client和server可以运行在同一台集群,也可以通过跨主机实现远程通信。

IMG_256

2 Docker镜像

Docker 镜像(Image)就是一个只读的模板。例如:一个镜像可以包含一个完整的操作系统环境,里面仅安装了 Apache 或用户需要的其它应用程序。镜像可以用来创建 Docker 容器,一个镜像可以创建很多容器。Docker 提供了一个很简单的机制来创建镜像或者更新现有的镜像,用户甚至可以直接从其他人那里下载一个已经做好的镜像来直接使用

镜像(Image)就是一堆只读层(read-only layer)的统一视角,也许这个定义有些难以理解,看看下面这张图:

无标题

右边我们看到了多个只读层,它们重叠在一起。除了最下面一层,其它层都会有一个指针指向下一层。这些层是Docker内部的实现细节,并且能够在docker宿主机的文件系统上访问到。统一文件系统(Union File System)技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件系统。

3 Docker容器

Docker 利用容器(Container)来运行应用。容器是从镜像创建的运行实例。它可以被启动、开始、停止、删除。每个容器都是相互隔离的、保证安全的平台。

可以把容器看做是一个简易版的 Linux 环境(包括root用户权限、进程空间、用户空间和网络空间等)和运行在其中的应用程序。

创建Container首先要有Image,也就是说Container是通过image创建的。

Container是在原先的Image之上新加的一层,称作Container layer,这一层是可读可写的(Image是只读的)。

在面向对象的编程语言中,有类跟对象的概念。类是抽象的,对象是类的具体实现。Image跟Container可以类比面向对象中的类跟对象,Image就相当于抽象的类,Container就相当于具体实例化的对象。

Image跟Container的职责区别:Image负责APP的存储和分发,Container负责运行APP。

无标题

最终,我们总结一下镜像和容器的关系:

  • 镜像:完整操作系统 + 需要运行的程序 所组成的一个文件
  • 容器:运行起来的文件

五、Docker的基本使用

1 Docker的基本命令

1.1 Hello World案例

下面,我们通过最简单的 image 文件"hello world",感受一下 Docker。

首先,运行下面的命令,将 image 文件从仓库抓取到本地。

shell docker pull library/hello-world

上面代码中,docker image pull是抓取 image 文件的命令。library/hello-world是 image 文件在仓库里面的位置,其中library是 image 文件所在的组,hello-world是 image 文件的名字。

由于 Docker 官方提供的 image 文件,都放在library组里面,所以它的是默认组,可以省略。因此,上面的命令可以写成下面这样。

docker pull hello-world

抓取成功以后,就可以在本机看到这个 image 文件了。

docker images

现在,运行这个 image 文件。

docker run hello-world

docker run命令会从 image 文件,生成一个正在运行的容器实例。

注意,docker run命令具有自动抓取 image 文件的功能。如果发现本地没有指定的 image 文件,就会从仓库自动抓取。因此,前面的docker image pull命令并不是必需的步骤。

如果运行成功,你会在屏幕上读到下面的输出。

docker run hello-world

输出这段提示以后,hello world就会停止运行,容器自动终止。

image-20251109220251526

对于那些不会自动终止的容器,必须使用以下命令命令手动终止。

docker kill [containID]

1.2 其他常用命令

除hello world中的案例以外,在实际工作中,以下命令我们也会常用以下容器运维命令:

列出容器:

# 列出本机正在运行的容器
docker ps 

# 列出本机所有容器,包括终止运行的容器
docker ps --all

查看容器中的日志:

docker logs [containID]

在运行中的容器内执行命令

docker exec
# 常用:
docker exec -it [containID] /bin/bash

docker的命令种类繁多,在这里我们仅介绍最基础,最常用的。因为我们的目标是成为一名大模型开发工程师或算法工程师,对于docker的高级用法,比如卷、网络等在实际工作中往往由运维负责,我们在这里不做赘述。

2 拓展:工作中对docker的使用

通过前面的学习,我们了解了怎么下载公开的镜像并使用已经打包并进行部署运行,以及常用的命令,在实际工作中,整体流程会略有不同。

首先是打镜像部分(具体怎么打,我们下个小结会讲),业务代码打包的镜像也不可能公开放到公网上,所以在实际工作中, 我们往往会把镜像推到一个私有镜像仓库中 。这个流程我们往往在本机或者开发机进行,执行一个 docker push 即可完成。

在部署我们写好的代码(一般是后端服务的形态)的时候,一般来讲我们都是

Docker镜像的打包和路径的映射关系 (2)

这部分内容提供给同学们作为拓展内容,目的是了解在实际工作中的工作流程,了解即可,不需要死记硬背。

3 构建一个Docker镜像

3.1 准备代码文件

完整的项目目录结构如下:

flask-blog-app/

├── app.py                 # Flask主应用
├── requirements.txt       # Python依赖
├── Dockerfile            # Docker构建配置
├── .dockerignore         # Docker忽略文件

├── templates/            # HTML模板目录
    └── index.html

我们先安装环境,并在本地执行

conda create -n dify-xa1 python==3.12.7

启动app.py以后,访问:http://localhost:5000,页面如下:

image-20251109231434216

3.2 编辑Dockerfile和忽略文件

Dockerfile 是 Docker 技术的核心组成部分之一,它是一个文本文件,里面包含了一系列用于自动化构建 Docker 镜像的指令。你可以把它理解为一份详细的“施工图纸”,Docker 引擎通过读取这份图纸,就能一步步地自动组装出一个可以直接运行应用程序的镜像。

注意:以下内容不需要强行记忆,在实际工作中,我们往往会借助使用文档会大模型编写以下文件,以及基于公司已有的Dockerfile(可能包含公司要求的一些规范)进行修改。请同学们把重点放到整体的流程的理解上:

# 使用官方Python精简镜像作为基础
FROM python:3.12.7-slim

# 设置元数据
LABEL version="1.0"
LABEL description="Simple Flask Blog API"

# 设置环境变量
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    FLASK_ENV=production \
    FLASK_DEBUG=False

# 设置工作目录
WORKDIR /app

# 安装系统依赖并清理缓存(合并RUN指令以减少镜像层数)
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get clean

# 复制依赖文件并安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 创建非root用户运行应用(增强安全性)
RUN groupadd -r flaskgroup && useradd -r -g flaskgroup flaskuser
RUN chown -R flaskuser:flaskgroup /app
USER flaskuser

# 暴露端口
EXPOSE 5000

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:5000/health || exit 1


# 设置容器启动命令[6](@ref)
CMD ["python", "app.py"]

常见指令如下:

指令 说明 示例
FROM 指定基础镜像,必须是 Dockerfile 的第一条有效指令(除了一些解析器指令) FROM python:3.9-slim
RUN 在构建过程中执行命令,用于安装软件包、创建目录等 RUN apt-get update && apt-get install -y git
COPY 将文件或目录从构建上下文复制到镜像中 COPY ./app /usr/src/app
ADD 功能类似 COPY,但源文件可以是 URL,如果是本地压缩文件还会自动解压(更推荐使用 COPY进行普通文件复制) ADD https://example.com/file.tar.gz /opt/
CMD 指定容器启动时默认执行的命令。Dockerfile 中可以有多个 CMD,但只有最后一个生效。docker run的命令行参数会覆盖 CMD ["python", "app.py"]
ENTRYPOINT 也用于指定容器启动时运行的命令,但不会被 docker run的命令行参数覆盖,而是将这些参数作为其本身的参数 ENTRYPOINT ["nginx", "-c"]
EXPOSE 声明容器运行时监听的网络端口,这只是一个文档性说明,并不会自动进行端口映射 EXPOSE 80
ENV 设置环境变量,这个变量在构建阶段和容器运行时都可用 ENV NODE_ENV production
WORKDIR 设置后续指令(如 RUN, CMD)的工作目录。如果目录不存在则会创建 WORKDIR /app
USER 指定运行后续指令以及运行容器时的用户名或 UID,有助于提高安全性 USER nobody

忽略文件:

.git
.gitignore
README.md
.env
*.pyc
__pycache__
*.log
Dockerfile
.dockerignore
venv/
env/
.venv
.coverage
htmlcov/
.pytest_cache/
.coverage.*

3.3 执行构建

执行以下命令:

# 构建镜像
docker build -t flask-blog-app .

# 或者指定标签
docker build -t flask-blog-app:1.0 .

构建过程如下:

image-20251109230416364

构建完毕后,执行docker images,可以看到镜像已经被放到了本地docker中

image-20251109232127944

3.4 运行容器

执行以下命令运行容器:

# 基本运行
docker run -d -p 5000:5000 --name blog-app flask-blog-app

# 带环境变量运行
docker run -d \
  -p 5000:5000 \
  -e FLASK_DEBUG=False \
  -e FLASK_PORT=5000 \
  --name blog-app \
  flask-blog-app

# 使用不同的主机端口
docker run -d -p 8080:5000 --name blog-app flask-blog-app

或者通过docker-compose运行

version: '3.8'

services:
  flask-app:
    build: .
    ports:
      - "5000:5000"
    environment:
      - FLASK_ENV=production
      - FLASK_DEBUG=False
    volumes:
      - ./logs:/app/logs
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

启动后,访问:http://localhost:5000 ,若页面和3.1中一致,说明已经成功启动。