Docker和虚拟机

一、镜像和虚拟环境有什么不一样?
  1. Python 虚拟环境(venv / conda)

它主要解决的是:

同一台机器上,不同 Python 项目的包版本冲突

比如:

  • 项目A要 torch==2.1

  • 项目B要 torch==1.13

你就可以给它们分别建不同虚拟环境。

虚拟环境通常只包含:

  • Python解释器

  • pip安装的包

  • 一些环境配置

但它依赖宿主机很多东西:

  • 宿主机操作系统

  • 宿主机系统库

  • 宿主机 CUDA / 驱动

  • 宿主机 shell、路径、权限等

所以虚拟环境只是:

Python层面的隔离

它不是完整环境打包。


  1. Docker 镜像,8个

Docker 镜像更大,它不是只管 Python 包,而是可以把这些一起打包:

  • 操作系统基础层(如 Ubuntu)

  • Python

  • PyTorch

  • CUDA runtime / toolkit

  • 你的 .py 代码

  • 配置文件

  • 启动命令

  • 甚至模型权重

所以镜像是:

应用运行环境的整体快照/模板

比虚拟环境范围大得多。


一句话对比

虚拟环境

只隔离 Python 包

镜像

打包 系统环境 + Python + 依赖 + 代码 + 运行方式

⚪完整流程
步骤 它在做什么 产物是什么
写 FastAPI / 模型代码 把业务功能写出来 源码
写 requirements.txt 固定 Python 依赖版本 依赖清单
写 Dockerfile 规定怎么构建运行环境 构建说明书
docker build 按 Dockerfile 做出镜像 image
docker run / compose 把镜像跑起来测试 container
CI 自动测试 + 构建镜像 自动检查代码并打包 测试结果 + 新镜像
push 到镜像仓库 把镜像上传到远程仓库 仓库中的镜像版本
生产服务器 / K8s 部署 在线上机器运行镜像 线上服务
health check + 监控 + 回滚 保证线上服务稳定 健康状态 / 可恢复能力
1)写 FastAPI / 模型代码

原话

写 FastAPI / 模型代码

人话翻译

先把你真正要提供的服务写出来。

比如:

  • 一个 /predict 接口

  • 一个 /health 接口

  • 模型加载逻辑

  • 数据预处理逻辑

  • 推理输出逻辑


它在干什么?

这是在定义:

你的服务到底要做什么事


输入是什么?

你的业务需求。

比如:

  • 输入一张图

  • 输出缺陷类别

  • 或输入 10 维特征

  • 输出类别 ID


输出是什么?

Python 源码文件。

比如:

  • main.py

  • model.py

  • schemas.py


没这一步会怎样?

后面一切都没意义,因为根本没有服务可打包。

假设你训练好了 SFDNet,你想让导师 / 同事 / 国外合作者都能用。

没有 FastAPI

  • 你把代码打包发给每个人;

  • 每个人自己装环境、自己装 CUDA、自己调 bug;

  • 每个人跑的结果可能略有差异(环境不一样);

  • 你改了模型要通知所有人重新拉代码;

  • 国外合作者可能根本没 GPU,用不了。

有 FastAPI + Docker

  • 你部署一个服务到学校服务器,地址 http://sfdnet.dhu.edu.cn:8000

  • 任何人(包括没 GPU 的)发 HTTP 请求就能用;

  • 所有人用的是同一个模型版本,结果完全一致;

  • 你升级模型,所有人下次调用自动用上新版;

  • 国外合作者在笔记本上点一下按钮就能跑。

这就是"服务"对"脚本"的本质超越------从"一份代码"变成"一项能力"


下次你看到"写 FastAPI / 模型代码"这句话,脑子里应该浮现的不是"一段处理数据的程序",而是:

一家准备开张营业的店铺,门口挂着菜单(API 接口),后厨备着厨师(模型),定好接单和出餐的规矩(schemas),接下来就等客人上门。

2)写 requirements.txt 固定依赖

原话

写 requirements.txt 固定依赖

人话翻译

把项目运行所需的 Python 包,以及它们的版本,明确写下来。

例如

fastapi==0.115.0

uvicorn==0.30.6

torch==2.4.1

numpy==1.26.4


它在干什么?

这是在回答:

这个项目到底依赖哪些 Python 库,版本是多少


输入是什么?

你的代码用了哪些包。

输出是什么?

一份依赖清单。

为什么要"固定版本"?

因为如果不固定:

  • 你今天装到 torch 2.4.1

  • 别人明天可能装到 torch 2.5.0

  • 结果行为可能不同

所以 requirements.txt 的本质是:

把 Python 依赖配方写死

3)写 Dockerfile 固定运行环境

原话

写 Dockerfile 固定运行环境

人话翻译

告诉 Docker:

你应该拿什么基础环境开始,装什么依赖,拷什么代码,最后怎么启动服务

例如:

FROM python:3.10.13-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install -r requirements.txt

COPY . .

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

它在干什么?

这是在写:

镜像怎么做出来的说明书

输入是什么?

  • 你的项目代码

  • requirements.txt

  • 你想用的基础镜像

  • 你希望的启动命令


输出是什么?

一个 Dockerfile 文本文件。


为什么它重要?

因为 Dockerfile 决定了:

  • 容器里 Python 版本是多少

  • 装哪些依赖

  • 代码放在哪里

  • 服务启动命令是什么

它相当于"标准化生产流程"。

4)docker build 生成镜像

原话

docker build 生成镜像

人话翻译

让 Docker 按 Dockerfile 的说明,真的把环境做出来。

命令例如:

docker build -t vision-infer:1.0.0 .


它在干什么?

就是把:

  • 基础镜像

  • Python 依赖

  • 系统包

  • 项目代码

打包成一个 image(镜像)


输入是什么?

  • Dockerfile

  • requirements.txt

  • 项目代码目录


输出是什么?

一个 image,比如:

  • vision-infer:1.0.0

为什么叫"镜像"?

因为它像一个静态模板,

还没真正跑起来,但已经把运行所需内容都准备好了。

镜像是一种"包",但和代码包的区别在于------它不只包含你的代码,还包含了一整套"能让这些代码跑起来的环境":操作系统层、系统库、CUDA、Python、依赖包、模型权重、启动配置。镜像 = "代码 + 完整运行环境"的不可变快照。

你说"输出是相对于只有所有代码的包"------这个方向对,但不够完整。准确的说法是:

镜像 = 代码 + 所有依赖 + 操作系统层 + 运行配置,打包成一个"随时能变成活容器"的文件。

把"镜像"和"代码包"做个对比你就懂了:

打包方式 里面有什么 能直接运行吗
ZIP 代码包 只有源代码 不能,对方要装 Python、装依赖、装 CUDA
Wheel / pip 包 代码 + Python 依赖声明 不能,对方要有 Python 环境和系统库
虚拟环境(venv) 代码 + Python + 依赖 能,但仅限同操作系统、同硬件架构
Docker 镜像 代码 + 依赖 + 完整 Linux 环境 + 运行配置 能,任何装了 Docker 的机器上都能跑,结果完全一致

所以"镜像"不只是"代码的包",而是**"整个运行环境的快照"**。它包含的东西远超你写的那几行代码。

二、镜像里到底装了什么------拆开看一下

还是用 SFDNet 那个例子。当你执行完 docker build -t sfdnet-inference:v1.0.0 .,生成的这个镜像文件(大约 4GB)里面实际上是这些东西堆起来的

复制代码
sfdnet-inference:v1.0.0 (4.2 GB)
│
├─── 第 1 层:Ubuntu 22.04 基础系统(约 70 MB)
│    └── 包含 /bin, /lib, /etc 等基本系统目录
│
├─── 第 2 层:CUDA 12.1 + cuDNN 8(约 2 GB)
│    ├── /usr/local/cuda/
│    ├── CUDA runtime 库
│    └── cuDNN 库
│
├─── 第 3 层:apt 装的系统包(约 100 MB)
│    ├── python3.10
│    ├── libglib2.0-0
│    └── libgl1 等
│
├─── 第 4 层:pip 装的 Python 依赖(约 1.8 GB)
│    ├── torch 2.1.0(最大的一个)
│    ├── fastapi
│    ├── uvicorn
│    ├── pillow
│    └── ...
│
├─── 第 5 层:你的代码(约 50 KB)
│    └── /app/main.py, model.py, inference.py
│
├─── 第 6 层:模型权重(约 200 MB)
│    └── /app/weights/sfdnet_best.pth
│
└─── 元数据(metadata)
     ├── 启动命令:CMD ["uvicorn", "app.main:app", ...]
     ├── 暴露端口:EXPOSE 8000
     ├── 环境变量:PYTHONUNBUFFERED=1 等
     ├── 工作目录:WORKDIR /app
     └── 健康检查配置

你的代码只占整个镜像的 0.001%(50KB / 4.2GB),剩下的 99.999% 都是为了让你的代码能跑起来的各种依赖和环境。这就是镜像和"代码包"最大的区别。

三、用 VMware 镜像做类比,最容易懂

你可能用过 VMware 或 VirtualBox------如果你从朋友那里拷来一个 Windows 虚拟机镜像,打开就是一个完整的 Windows 系统,里面装好的软件、文件、设置都在。

Docker 镜像概念上完全一样------它是一个"迷你 Linux 系统的快照",只不过:

  • VMware 镜像:完整虚拟机,几十 GB,启动慢;

  • Docker 镜像:只含必要组件,共享宿主机内核,几 GB,启动毫秒级。

所以镜像的本质是:"一台随时可以复刻出来的虚拟电脑的快照"。里面包含的不只是代码,而是"代码 + 能让代码跑起来的一切东西"。

5)docker run / docker compose 本地测试

原话

docker run / docker compose 本地测试

人话翻译

把刚才 build 出来的 image 真的启动起来,看看它能不能跑。

例如:

docker run -p 8000:8000 vision-infer:1.0.0

或者:

docker compose up --build


它在干什么?

这是把 image 变成一个运行中的 container。


输入是什么?

一个 image。


输出是什么?

一个正在运行的 container。


为什么要本地测试?

因为你刚 build 出来的镜像,未必真能工作。

常见问题有:

  • 启动命令写错

  • 缺包

  • 端口没暴露

  • 路径不对

  • health 接口报错

所以这一步是在做:

上线前的第一轮验证


6)CI 自动测试 + 自动构建镜像

原话

CI 自动测试 + 自动构建镜像

人话翻译

CI = Continuous Integration,持续集成。

意思是:

你一提交代码,系统就自动帮你做检查

比如 GitHub Actions、GitLab CI、Jenkins。


它在干什么?

自动完成这些事情:

  • 拉代码

  • 安装依赖

  • 跑单元测试

  • 构建 Docker image


输入是什么?

你 push 到 Git 仓库的新代码。


输出是什么?

  • 测试通过 / 失败结果

  • 一个新的镜像版本


为什么要自动做?

因为如果全靠人手动:

  • 容易漏测

  • 容易忘 build

  • 每个人操作不一致

CI 的价值就是:

把检查流程机器化、稳定化


7)push 到镜像仓库

原话

push 到镜像仓库

人话翻译

把刚才 build 出来的 image 上传到远程仓库,方便别人拉取。

比如:

  • Docker Hub

  • Harbor

  • AWS ECR

  • GitHub Container Registry


它在干什么?

相当于把"产品成品"放进仓库。


输入是什么?

本地 build 好的 image。


输出是什么?

远程仓库里的一份镜像版本。

例如:

  • registry.example.com/vision-infer:1.0.0

为什么需要这一步?

因为生产服务器不可能从你电脑里拿镜像。

它必须从一个统一的远程仓库去拉。

所以这一步本质是:

把镜像从开发机交付给部署系统


8)生产服务器 / Kubernetes 拉取镜像部署

原话

生产服务器 / Kubernetes 拉取镜像部署

人话翻译

让线上机器真正去运行这个镜像,对外提供服务。


两种常见方式

方式 A:服务器直接 docker run

适合小项目。

方式 B:Kubernetes 部署

适合更工业化场景,支持:

  • 多副本

  • 自动重启

  • 滚动更新

  • 自动扩缩容


它在干什么?

这是在把"准备好的成品"真正上线。


输入是什么?

远程镜像仓库里的 image。


输出是什么?

线上运行中的服务实例。


为什么不能直接用开发机上的 container?

因为开发机不是生产环境:

  • 不稳定

  • 不可持续

  • 不适合对外服务

生产部署这一步,才是把服务正式交付给用户。


9)health check + 监控 + 回滚

原话

health check + 监控 + 回滚

人话翻译

服务上线后,不是就完了,

还要持续盯着它有没有出问题。


9.1 health check 是什么?

健康检查。

比如系统定时访问:

GET /health

如果返回正常,就说明服务活着。


它解决什么问题?

不是"进程还在"就说明服务能用。

可能进程没死,但:

  • 模型没加载成功

  • 数据库连不上

  • 接口卡死

  • 内存爆了快不行了

所以 health check 是在判断:

服务是不是健康可用


9.2 监控是什么?

持续收集运行指标。

比如:

  • CPU 使用率

  • 内存使用率

  • 接口响应时间

  • 错误率

  • QPS

  • 容器重启次数


它的作用是什么?

让你知道:

  • 服务有没有变慢

  • 有没有异常增多

  • 有没有资源快耗尽


9.3 回滚是什么?

如果新版本上线后出问题,

快速切回旧版本。

例如:

  • vision-infer:1.0.1

  • 切回 vision-infer:1.0.0


为什么回滚很重要?

因为生产事故时,

最快的修复方式往往不是"现场改代码",

而是:

直接切回上一个稳定镜像

这就是 Docker 版本化带来的巨大价值。

⚪Docker 到底统一了什么?

Docker 统一的是应用运行时依赖的 user space 环境,不是把整个操作系统和硬件都复制一遍。

具体内容 Docker image 会不会带上 是否统一
应用层 你的代码、程序 能统一
依赖层 Python、torch、libc、系统包 能统一
文件系统层 /app/usr/bin 这些目录内容 能统一
Kernel 层 Linux kernel / Windows kernel 不能由 image 自带
硬件层 CPU、GPU、驱动 不能完全统一

不用 Docker vs 用 Docker

问题维度 不用 Docker 用 Docker
Python 版本 每台机器自己装,可能不同 image 里固定
第三方库版本 可能冲突 image 里固定
系统包依赖 Ubuntu/CentOS/macOS 差异很大 image 里固定
目录结构 各自不同 image 里固定
Kernel 差异 存在 仍然存在
GPU / 驱动差异 存在 仍然存在
⚪如果没有 Docker,会出什么问题?

假设两个人都开发同一个 Python 服务。

员工 A(Windows)

  • Python 3.11

  • 某个 wheel 安装方式

  • 路径分隔符 \

  • 本地自己配的环境变量

员工 B(Linux)

  • Python 3.9

  • gcc / libc 版本不同

  • 路径分隔符 /

  • pip 装的库版本也不同

结果:

  • A 能跑

  • B 不能跑

  • 部署机又是 Ubuntu,还是第三套环境

这时候问题通常不是 kernel 先出事

而是前面这些更高频的东西先炸:

  • Python 版本

  • torch 版本

  • 依赖包

  • 系统库

  • 路径

  • 命令工具

  • 文件系统布局


⚪所以 Docker 在解决什么?

Docker 在解决的是:

绝大多数"应用级、依赖级、用户空间级"的环境不一致问题。

它不是在说:

"我连内核都给你变得一模一样"

它是在说:

"至少你应用最常碰到的那一层,我给你钉死了。"

"假设两个员工 a 和 b 一起做项目,a 创了一个 Docker,b 也创了一个 Docker,那是不是相当于在这个 Docker 里面他俩包装的版本都一样?"

人话翻译

不一定。

这取决于他们是不是:

  • 用同一个 Dockerfile

  • 用同一个基础镜像

  • 用同一个 requirements.txt

  • 把版本锁死了

  • 用同一个项目代码快照

左边:Container

Server

真实硬件:CPU / 内存 / 磁盘 / 网卡

OS Kernel

宿主机内核,负责调度进程、分配内存、管理文件和网络

Container runtime

负责把容器创建出来,做好隔离、挂载、网络、资源限制

Bin / Lib / module

这个应用自己需要的解释器、依赖库、模块

App

真正要运行的业务程序


右边:Virtual Machine

Server

真实硬件

Hypervisor

把一台真实机器切成多台虚拟机器

Guest OS

每个 VM 自己的操作系统

Bin / Lib / module

这个 VM 内应用自己的依赖

App

真正运行的业务程序


最后再给你一个"逐词秒懂版"

左边容器图

  • App:应用程序

  • Bin / Lib / module:应用依赖

  • Container runtime:容器运行管理层

  • OS Kernel:宿主机内核

  • Server:真实物理服务器

右边虚拟机图

  • App:应用程序

  • Bin / Lib / module:应用依赖

  • Guest OS:虚拟机自己的操作系统

  • Hypervisor:虚拟化管理层

  • Server:真实物理服务器

相关推荐
A-刘晨阳2 小时前
k8s之镜像拉取策略
运维·docker·容器·kubernetes·运维开发·harbor
IMPYLH2 小时前
【无标题】
linux·运维·服务器·网络·bash
Elivs.Xiang2 小时前
Redis - Docker环境下的持久化、主从复制、哨兵、集群、淘汰策略
数据库·redis·docker
比昨天多敲两行2 小时前
Linux权限管理
linux·运维·服务器
runningshark2 小时前
【Linux】VirtualBox ↔ Ubuntu+WinSCP 文件传输
linux·运维·ubuntu
MXsoft6182 小时前
信创时代的运维“铁三角”:一体化监控、自主底座与A预判
运维
aidream12393 小时前
Linux文件操作-文件打包和压缩(tar/gzip/bzip2/xz/zip)
linux·运维·服务器
迷茫运维路3 小时前
云枢运维管理系统
运维·golang·kubernetes·gin·casbin
feng_you_ying_li3 小时前
linux之进程优先与切换调度
linux·运维·服务器