Docker 深度指南:从部署博客到理解容器
这不是一本"命令大全",而是一条从真实困境出发、逐层剥开的认知路径。
每个概念都在你问"为什么"的时候才出现,每个流程图都试图把一个抽象机制说清楚。
引子:我的博客,三个环境的三种命运
你写好了一个博客——Python + FastAPI,本地 uvicorn app.main:app 跑得飞起。
准备上线了:
开发机 (Python 3.12) 测试服 (Python 3.8) 生产服 (Python 3.10)
───────────────────── ────────────────── ──────────────────
uvicorn 启动 ✅ pip install 报错 ❌ 启动成功但 500 ❌
import 全正常 语法兼容问题 配置路径不同
.env 有值 .env 没传 .env 值不对
这还没算 Redis 版本、Nginx 配置、系统依赖(build-essential、libssl 之类)……
核心矛盾:代码可以 git 同步,但运行环境无法 git 同步。
传统部署 Docker 部署
────────────────────────────────── ──────────────
代码 push → 登录服务器 → 装 Python 代码 push → docker build →
→ 创建虚拟环境 → pip install docker compose up -d
→ 配 Nginx → 配 systemd → 祈祷启动成功 环境即代码,到哪都一样
这就是 Docker 要回答的问题:能不能把代码和运行环境一起打包?
第一层:把环境一起打包 — 镜像
1.1 一张图说清 Docker 在做什么
┌──────────────────────────────────────────────────────────────┐
│ Docker 工作流总览 │
│ │
│ Dockerfile ──build──▶ 镜像(Image) ──run──▶ 容器(Container) │
│ (配方/定义) (打包模板) (运行实例) │
│ │
│ 一次 build ↓ push/pull │
│ 到处 run Registry 仓库 │
│ (Docker Hub/阿里云) │
└──────────────────────────────────────────────────────────────┘
两张图对比传统部署和 Docker 部署,核心差异在于:Docker 交付的不只是代码,而是整个用户空间文件系统快照。
1.2 往上拉一层看:Docker 系统由哪些角色构成
你敲的命令 真正干活的进程 镜像存放处
docker build dockerd(守护进程) Docker Hub
docker run ──▶ 监听 /var/run/docker.sock 阿里云 ACR
docker compose 管理镜像/容器/网络/卷 Harbor(私有)
│ │ │
客户端 服务端 仓库
关键认知:你敲的每一个 docker xxx 命令,都是通过 Unix Socket 向 dockerd 发请求。容器不是"命令创建"的,是 dockerd 分配了网络命名空间、veth 对、文件系统层之后产生的。
1.3 镜像里到底有什么?没有什么?
这是理解 Docker "轻"在哪里的关键。
python:3.12-slim 镜像(约 150MB)
┌──────────────────────────────────────────┐
│ 你的应用程序 + 依赖 │
│ (Flask, numpy, torch...) │
├──────────────────────────────────────────┤
│ Python 运行时 + pip │
│ (/usr/local/bin/python, .../lib/...) │
├──────────────────────────────────────────┤
│ Debian 用户空间文件 │
│ (/bin, /usr/lib, /etc, ... 不含内核!) │
├──────────────────────────────────────────┤
│ ~~~ 这里没有 Linux 内核 ~~~ │
│ 内核由宿主机提供,容器共享宿主机内核 │
└──────────────────────────────────────────┘
graph TB
subgraph 虚拟机架构
A1[App A] --> G1[Guest OS
完整内核 + 用户空间]
A2[App B] --> G2[Guest OS
完整内核 + 用户空间]
G1 --> H1[Hypervisor 虚拟化层]
G2 --> H1
H1 --> HK1[Host OS 内核]
HK1 --> HW1[物理机]
end
subgraph Docker架构
B1[App A] --> C1[容器
仅用户空间文件]
B2[App B] --> C2[容器
仅用户空间文件]
C1 --> D1[Docker Engine]
C2 --> D1
D1 --> HK2[Host OS 内核
仅此一份]
HK2 --> HW2[物理机]
end
style G1 fill:#ffcccc,stroke:#ff0000
style G2 fill:#ffcccc,stroke:#ff0000
style C1 fill:#ccffcc,stroke:#00aa00
style C2 fill:#ccffcc,stroke:#00aa00
镜像大小 内核 启动时间 隔离强度
──────────────────────────────────────────────────────────────────
VM: GB 级 每 VM 一个 分钟级 硬件级(强)
容器: MB 级 共享宿主机 秒级/毫秒 进程级(中)
所以 FROM python:3.12-slim 这句话不是在装一个"轻量虚拟机",而是在往镜像里塞一个精简版 Debian 的用户空间文件。
1.4 Docker 依赖的三个内核特性
这三个东西是理解"容器到底是什么"的钥匙:
Linux 内核
│
├── Namespace(命名空间)
│ "你能看到什么?"
│ 隔离进程视图(PID)、网络(NET)、文件系统(MNT)、用户(USER)……
│ → 容器里的 ps 只能看到自己的进程
│ → 容器里的 ifconfig 只能看到自己的网卡
│
├── Cgroups(控制组)
│ "你能用多少资源?"
│ 限制 CPU、内存、磁盘 I/O
│ → --cpus=1.5 --memory=1g
│
└── UnionFS / OverlayFS(联合文件系统)
"你看到的文件系统是怎么来的?"
多个只读层 + 一个可写层 → 合并视图
→ 层共享 = 磁盘只存一份 = 拉取时 Already exists
一句话总结本质:容器 = 被 Namespace 限制视野 + 被 Cgroup 限制资源 + 被 UnionFS 提供文件系统视图的宿主机进程。
第二层:定义"打包" — Dockerfile
2.1 场景:你写了一篇新博客,想把应用打包成镜像
你需要告诉 Docker:基于什么系统、装什么工具、安装什么依赖、把代码放哪、启动时执行什么。这就是 Dockerfile。
2.2 指令速览
| 指令 | 做的事 | 对镜像层的影响 |
|---|---|---|
FROM |
指定基础镜像 | 第一层(底层) |
WORKDIR |
设置容器内工作目录 | 不产生新层 |
ENV |
设置环境变量 | 产生新层 |
RUN |
在构建时执行命令 | 产生新层(每 RUN 一层) |
COPY |
从宿主机复制文件到镜像 | 产生新层 |
EXPOSE |
声明容器监听的端口(文档性质) | 不产生新层 |
CMD |
容器启动时的默认命令 | 不产生新层 |
VOLUME |
声明挂载点 | 产生新层 |
2.3 一条指令,一层文件系统
Dockerfile 构建过程
───────────────────── ─────────────────────
FROM python:3.12-slim → Layer 1: Debian 用户空间 (150MB)
RUN apt-get install curl → Layer 2: curl 二进制 + lib 的增量 (5MB)
COPY requirements.txt . → Layer 3: 文件内容写入 (1KB)
RUN pip install -r ... → Layer 4: site-packages/ 下的文件 (50MB)
COPY . . → Layer 5: 你的代码 (10MB)
最终镜像 ≈ 150 + 5 + 0.001 + 50 + 10 = 215MB(只读层叠加)
flowchart LR
subgraph Dockerfile
A[FROM python:3.12-slim]
B[RUN apt-get install curl]
C[COPY requirements.txt .]
D[RUN pip install -r requirements.txt]
E[COPY . .]
end
subgraph 镜像层
L1[Layer 1: base
150MB]
L2[Layer 2: curl
5MB]
L3[Layer 3: req.txt
1KB]
L4[Layer 4: pip pkgs
50MB]
L5[Layer 5: code
10MB]
end
A --> L1
B --> L2
C --> L3
D --> L4
E --> L5
L5 --> L4 --> L3 --> L2 --> L1
2.4 一个典型 Python 项目的 Dockerfile
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
TZ=Asia/Shanghai
# 第一层 RUN:系统依赖(变动最少,放最前)
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' \
/etc/apt/sources.list.d/debian.sources \
&& apt-get update && apt-get install -y --no-install-recommends \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/*
# 第二层:Python 依赖(先 COPY 依赖文件,不 COPY 业务代码)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt \
-i https://mirrors.aliyun.com/pypi/simple/ \
--trusted-host mirrors.aliyun.com
# 第三层:业务代码(最后才 COPY)
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
这个顺序不是随便写的——它直接决定了你 docker build 能跑多快。
第三层:为什么改一行代码,构建有时秒过有时五分钟?
3.1 层缓存的工作方式:内容哈希
Docker 不"记住上次做了什么",它对每一层的内容算 SHA256 哈希:
Layer: COPY requirements.txt .
→ 文件内容 SHA256 = abc123...
Layer: RUN pip install -r requirements.txt
→ 上一层的哈希(abc123) + 命令字符串 + RUN 输出的内容 → SHA256 = def456...
缓存命中的逻辑简单到只有一个判断:
flowchart TD
A[Dockerfile 第 N 条指令] --> B{该指令文本
和输入文件
和上层的 SHA256
都和上次一样?}
B -->|是| C[✅ CACHED
跳过,直接用缓存层]
B -->|否| D[🔄 重新执行此指令
生成新层]
D --> E[第 N+1 条指令
也必须重新执行]
E --> F[第 N+2 条指令
也必须重新执行]
F --> G[...
之后所有层全部重建]
一层变了,后面的全得重跑。
3.2 所以层顺序就是一切
场景:你只改了 blog.py 里的一行文字
✅ 正确顺序(从不变到常变):
──────────────────────────────────────────────
[1/7] FROM python:3.12-slim ✅ CACHED 0.0s
[2/7] WORKDIR /app ✅ CACHED 0.0s
[3/7] apt-get install... ✅ CACHED 0.0s
[4/7] COPY requirements.txt . ✅ CACHED 0.0s
[5/7] pip install -r ... ✅ CACHED 0.0s
[6/7] COPY . . 🔄 执行 2.1s
[7/7] CMD ... ✅ CACHED 0.0s
──────
总计: ~2s
❌ 错误顺序(代码 COPY 在最前):
──────────────────────────────────────────────
[1/7] FROM python:3.12-slim ✅ CACHED
[2/7] COPY . . 🔄 执行 ← 代码变了,这层变了
[3/7] RUN pip install ... 🔄 重新装全部依赖(哪怕没加新包)
[4/7] ... 后面全重跑
──────
总计: ~5 min
核心原则:把变动频率低的放前面,变动频率高的放后面。
依赖安装 < 系统工具安装 < 基础镜像 < 业务代码 COPY
(偶尔变) (几乎不变) (极少变) (每次改)
─────────────────────────────────────────────────────────────▶
先 ◀────────── Dockerfile 中的位置 ──────────▶ 后
3.3 深入一层:OverlayFS 读写过程
前面说了"层",那容器运行时这些层是怎么组合起来的?
flowchart TB
subgraph "容器看到的文件系统(合并视图)"
APP[容器内进程看到的
完整文件系统]
end
subgraph "OverlayFS 四层叠加"
direction TB
CW[Container Layer
可写层
容器产生的所有变更]
L3[Image Layer 3
只读: COPY . .]
L2[Image Layer 2
只读: pip install 的库]
L1[Image Layer 1
只读: FROM python:3.12-slim]
end
CW -.->|写时复制| L3
L1 --> APP
L2 --> APP
L3 --> APP
CW --> APP
subgraph "操作规则"
R1[读文件: 从上往下逐层查找
找到即返回]
R2[写文件: Copy-on-Write
先把文件复制到可写层
再在可写层修改]
R3[删除文件: 在可写层创建
whiteout 遮蔽标记
底层文件不受影响]
end
读 /usr/bin/python:
可写层没有 → Layer 3 没有 → Layer 2 没有 → Layer 1 有 → 返回
写 /tmp/debug.log:
文件在 Layer 1 里?→ 先 Copy-on-Write 复制到可写层 → 在可写层写
新文件?→ 直接在可写层创建
删 /app/old_file.txt:
在可写层创建一个 whiteout 文件(标记"这个文件不存在")
Layer 3 里的 old_file.txt 还在,但合并视图里看不到它
两个容器共享同一个 python:3.12-slim 层 → 磁盘上这 150MB 只存一份 → 各自的可写层分开 → 互不影响。
这就是为什么 10 个容器可以共用同一个 Python 基础层,磁盘不会膨胀 10 倍。
3.4 基础镜像怎么选
python:3.12 ~1.0GB 完整版 Debian + 编译工具链,适合开发调试
python:3.12-slim ~150MB 精简 Debian,去掉文档/非必要包 ← 推荐生产
python:3.12-alpine ~50MB 基于 Alpine Linux(musl libc),极致小
Alpine 的坑(经验教训):
Alpine 用 musl libc 而不是 glibc。
对于纯 Python Web 服务,一般没问题。
但遇到以下场景要小心:
- C 扩展编译(numpy、pandas、torch):可能找不到预编译 wheel
- 神秘的 "symbol not found" / "ImportError":排查半天发现是 musl/glibc 不兼容
- 某些 Python 包依赖的 .so 文件在 Alpine 下行为不同
经验法则:普通 Web 服务用 slim,C/科学计算依赖多用 slim,
只有确认兼容时才用 alpine
3.5 .dockerignore:别往镜像里塞垃圾
# Python 编译缓存
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/
# 版本控制
.git/
# 敏感信息
.env
.env.*
# 本地数据和模型(体积巨大,不应打包进镜像)
data/
models/
*.db
*.sqlite3
# IDE 和文档
.vscode/
.idea/
*.md
tests/
每一条被忽略的文件,都不会被发送到 dockerd 的构建上下文里,直接加快 COPY . . 的速度。
3.6 多阶段构建:让最终镜像只含运行时需要的东西
flowchart LR
subgraph "阶段1: builder"
B1[FROM python:3.12 AS builder
含编译工具 gcc/make]
B2[COPY requirements.txt .]
B3[RUN pip install ...
安装到 /root/.local]
end
subgraph "阶段2: runtime(最终镜像)"
R1[FROM python:3.12-slim AS runtime
不含编译工具]
R2[COPY --from=builder
/root/.local /root/.local]
R3[COPY . .]
R4[CMD ...]
end
B1 --> B2 --> B3
B3 -.->|只复制产物| R2
R1 --> R2 --> R3 --> R4
subgraph "结果对比"
D1[构建阶段镜像: ~1.2GB
不推送,仅用于构建]
D2[运行阶段镜像: ~300MB
推送到仓库]
end
# ============ 构建阶段 ============
FROM python:3.12 AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt \
-i https://mirrors.aliyun.com/pypi/simple/ \
--trusted-host mirrors.aliyun.com
# ============ 运行阶段 ============
FROM python:3.12-slim AS runtime
WORKDIR /app
# 只从 builder 复制已安装的 Python 包(不含 gcc 等编译工具)
COPY --from=builder /root/.local /root/.local
# 最后复制代码
COPY . .
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
这个模板你现在就能用。构建阶段有 gcc,但最终镜像里没有。
第四层:博客 + Redis + Nginx — 多服务怎么办?
4.1 场景扩展
博客不只是 Python 应用,需要:
用户 → Nginx(反向代理 + 静态文件)
│
▼
博客应用(FastAPI,处理请求)
│
▼
Redis(缓存、会话)
三个服务,需要一起启动、联通网络、数据不丢。Docker Compose 就是干这个的。
4.2 docker-compose.yml 逐段拆解
version: "3.9"
# ── 可复用配置(YAML 锚点,减少重复)──
x-logging: &default-logging
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
x-restart: &default-restart
restart: unless-stopped
services:
# ── 主应用 ──
app:
build: . # 从当前目录 Dockerfile 构建
container_name: blog_app
<<: *default-restart
ports:
- "8000:8000" # 宿主机:容器
volumes:
- ./data:/app/data # 数据持久化
environment:
- REDIS_URL=redis://redis:6379/0
networks:
- internal
logging: *default-logging
# ── Redis 缓存 ──
redis:
image: redis:7-alpine
container_name: blog_redis
<<: *default-restart
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
- internal
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
redis_data:
networks:
internal:
driver: bridge
4.3 docker compose up 背后发生了什么?
这是理解多容器协作的关键。
sequenceDiagram
participant U as 你执行
docker compose up
participant C as docker compose
participant D as dockerd
U->>C: docker compose up -d
C->>C: 解析 docker-compose.yml
C->>D: 创建网络 "internal"(bridge 类型)
D-->>D: 分配子网 172.18.0.0/16
D-->>C: 网络就绪
C->>D: 拉取 redis:7-alpine 镜像
D-->>C: 镜像就绪
C->>D: 创建 redis 容器
D-->>D: 分配 IP 172.18.0.2
D-->>D: 加入 internal 网络
D-->>D: 启动内置 DNS(127.0.0.11:53)
D-->>D: 注册容器名 redis → 172.18.0.2
C->>D: 构建 app 镜像(Dockerfile)
D-->>C: 镜像就绪
C->>D: 创建 app 容器
D-->>D: 分配 IP 172.18.0.3
D-->>D: 加入 internal 网络
D-->>D: 注册容器名 app → 172.18.0.3
D-->>D: 添加 iptables DNAT 规则
宿主机:8000 → 172.18.0.3:8000
C-->>U: 所有服务启动完毕
注意:depends_on 只控制启动顺序,不确保服务真正就绪。Redis 可能还没加载完数据,app 就启动了。生产环境需要在启动命令里加入等待逻辑(如 wait-for-it.sh)或依赖健康检查。
4.4 开发 vs 生产配置分离
项目结构:
project/
├── docker-compose.yml # 基础配置(共用)
├── docker-compose.override.yml # 开发覆盖(docker compose up 自动合并)
├── docker-compose.prod.yml # 生产覆盖(手动指定 -f)
├── .env # 敏感变量(不提交 git)
├── .env.example # 变量模板(提交 git)
└── Dockerfile
# docker-compose.override.yml(开发环境,自动合并)
services:
app:
volumes:
- .:/app # 挂载源码,修改实时生效
environment:
- DEBUG=true
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
# 开发
docker compose up -d --build # 自动合并 override.yml
# 生产
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
4.5 何时需要 --build
# 需要 --build 的场景:
# - 改了 Dockerfile
# - 改了 requirements.txt
# - 改了 uv.lock / pyproject.toml
# - 第一次启动
docker compose up -d --build
# 不需要 --build 的场景:
# - 只改了业务代码,且代码通过 volume 挂载进去的
# - 只是重启服务
docker compose restart
docker compose up -d # 不加 --build,用已有镜像
第五层:一个请求的完整旅程 — 网络核心
这是整个文档中最重要的一节。理解网络,你就理解了 Docker。
5.1 端口映射的本质:不是把端口"打开",是加了 NAT 规则
很多人以为 ports: - "8080:8000" 是"让容器的 8000 端口能被外面访问"。不完全是。
docker compose 里写的:
ports:
- "8080:8000"
dockerd 实际做的事:
在宿主机 iptables 里加了一条 DNAT 规则:
目的地址 = 宿主机 IP,目的端口 = 8080
→ 改写目的地址为 容器 IP (172.18.0.3)
→ 改写目的端口为 8000
用 iptables 查看:
$ iptables -t nat -L DOCKER
DNAT tcp -- anywhere anywhere tcp dpt:8080 to:172.18.0.3:8000
外部世界 宿主机 容器
──────── ────── ─────
浏览器 iptables app:8000
│ │ │
│──→ 宿主机:8080 ──→ DNAT 改写目标 ──→ 172.18.0.3:8000 ──│
│ │ │
│←── 返回 ←── 反向 SNAT ←── 172.18.0.3:8000 ←─────────│
容器内部完全不感知端口映射。容器里的应用只管监听 8000,至于外面用 8080 还是 9090 访问,它不知道也不关心。
5.2 容器间怎么互相找到对方?
方式一(默认 bridge):通过 IP 通信 → 但 IP 重启会变 → --link(已废弃)
方式二(自定义网络):通过容器名通信 → 内置 DNS 自动解析 → ✅ 推荐
两个 key 事实:
- 同一自定义网络内的容器,不需要端口映射就能互访
- 容器名自动注册为 DNS 的 A 记录
web 容器内部:
$ ping redis
PING redis (172.18.0.2) 56(84) bytes of data.
64 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.123 ms
这是怎么工作的?
web 容器的 /etc/resolv.conf 指向 127.0.0.11:53(Docker 内置 DNS)
当代码调用 redis.get() 时:
1. Python 解析主机名 "redis"
2. glibc 查 /etc/resolv.conf → 向 127.0.0.11:53 发 DNS 请求
3. Docker DNS 返回 172.18.0.2(redis 容器的 IP)
4. 建立 TCP 连接到 172.18.0.2:6379
5.3 自定义网络 vs 默认 bridge 对比
特性 默认 bridge 自定义网络(推荐)
──────────────────────────────────────────────────────────
容器名解析 需要 --link(废弃) 内置 DNS 自动解析
跨容器通信 默认不互通 同一网络内默认互通
网络隔离 所有容器在同一桥 可建多个独立网络
重启后 IP 可能变 可能变(但用容器名,不影响)
5.4 完整请求链路逐跳追踪
以你的博客为例:用户访问 http://宿主机:8080/article/1,博客应用查询 Redis 获取缓存数据。
sequenceDiagram
participant B as 浏览器
participant H as 宿主机
iptables
participant B0 as bridge (br-xxx)
participant W as web 容器
172.18.0.3:8000
participant DNS as Docker DNS
127.0.0.11:53
participant R as redis 容器
172.18.0.2:6379
B->>H: TCP SYN → 宿主机IP:8080
H->>H: iptables DNAT
宿主机:8080 → 172.18.0.3:8000
H->>B0: 数据包进入 bridge
B0->>W: 转发到 web 容器的 eth0
Note over W: FastAPI 收到请求
执行 redis.get(key)
W->>DNS: 查询 "redis" → ?
DNS-->>W: 172.18.0.2
W->>B0: TCP SYN → 172.18.0.2:6379
B0->>R: 转发到 redis 容器的 eth0
R-->>W: 返回缓存数据
Note over W: FastAPI 生成 HTTP 响应
W->>B0: HTTP 响应 → bridge
B0->>H: 宿主机 iptables 反向 SNAT
H->>B: HTTP 200 → 浏览器
关键洞察:
容器间通信(web→redis):
web 容器 eth0 → veth pair → 宿主机 bridge 桥 br-xxx → redis 容器 eth0
走的是二层桥接,不经过宿主机 iptables 的 NAT 链 → 效率高、延迟低
外部访问容器(浏览器→web):
浏览器 → 宿主机 iptables DNAT → bridge → web 容器
经过 NAT 转换,有一点额外开销(但通常可忽略)
veth pair 是什么?你可以把它理解为"一根网线的两端":一端插在容器里(eth0),另一端插在宿主机的 bridge 桥上。数据从一头进,另一头出。
第六层:进入生产 — 依赖管理、安全、国内网络
6.1 Python 依赖管理的进化:从 pip 到 uv
2010 ── pip + requirements.txt
问题:flask>=2.0 → 今天装 2.3,明天装 2.4,行为不一致
没有锁文件,没有依赖冲突检测
2017 ── pipenv / poetry
有了锁文件(Pipfile.lock / poetry.lock)
但依赖解析慢(纯 Python 实现),大型项目几分钟
2024 ── uv(Astral 团队,Rust 编写)
快 10-100 倍,有 uv.lock 精确锁版本
区分直接依赖和间接依赖
传统 pip:pip install -r requirements.txt → 约 60-120 秒
uv: uv sync --frozen → 约 3-10 秒
uv 的项目结构:
# pyproject.toml
[project]
name = "my-blog"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.110.0",
"uvicorn>=0.29.0",
"chromadb>=0.4.0",
]
[tool.uv]
dev-dependencies = [
"pytest>=8.0.0",
"ruff>=0.4.0",
]
uv add fastapi uvicorn # 添加依赖 → 更新 pyproject.toml + uv.lock
uv sync # 按 uv.lock 精确安装
uv run uvicorn app.main:app # 在项目环境里运行
Docker + uv 的最佳实践:
FROM python:3.12-slim
# 从官方镜像直接拿 uv 二进制(不需要 pip install uv)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_SYSTEM_PYTHON=1 \ # 安装到系统 Python(Docker 里不需要虚拟环境)
UV_COMPILE_BYTECODE=1 \ # 预编译 .pyc
UV_NO_CACHE=1 # 构建时不缓存(减小镜像)
# 先复制依赖锁文件(利用缓存:锁文件不变 → 依赖层走缓存)
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev \
--index-url https://mirrors.aliyun.com/pypi/simple/
# 最后复制代码
COPY . .
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
6.2 安全加固:几条关键的
# 1. 非 root 用户运行
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
RUN chown -R appuser:appuser /app
USER appuser # 此后所有指令以 appuser 执行
# 2. 使用 exec 格式的 CMD(PID 1 直接是应用,能收到 SIGTERM)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# 3. 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# docker-compose.yml 层面的安全限制
services:
app:
read_only: true # 容器文件系统只读
tmpfs:
- /tmp # 临时文件用内存盘
security_opt:
- no-new-privileges:true # 禁止提权
cap_drop:
- ALL # 删除所有 Linux capabilities
cap_add:
- NET_BIND_SERVICE # 只加这一个(允许绑定低端口)
敏感信息永远不要写进 Dockerfile:
# ❌ 这行会留在镜像层里,docker history 能看到
ENV SECRET_KEY=abc123
# ✅ 通过 .env 文件注入(.env 已加入 .gitignore)
6.3 国内网络加速:四层全配置
flowchart TD
subgraph "Docker 构建中的四层网络瓶颈"
L1[Layer 1: apt-get
访问 deb.debian.org]
L2[Layer 2: pip install
访问 pypi.org]
L3[Layer 3: HuggingFace
下载模型文件]
L4[Layer 4: Docker Hub
拉取基础镜像]
end
L1 --> S1[换清华 Debian 源]
L2 --> S2[换阿里云 PyPI 源]
L3 --> S3[设置 HF_ENDPOINT
=https://hf-mirror.com]
L4 --> S4[配置 registry-mirrors
腾讯云/中科大]
# 完整加速 Dockerfile 片段
# Layer 1: Debian apt 换源
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' \
/etc/apt/sources.list.d/debian.sources
# Layer 2: pip 换源
RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ \
&& pip config set global.trusted-host mirrors.aliyun.com
# Layer 3: HuggingFace 换源
ENV HF_ENDPOINT=https://hf-mirror.com
# Layer 4: PyTorch CPU 版(阿里云源,可选)
RUN pip install torch \
-i https://mirrors.aliyun.com/pypi/simple/ \
--trusted-host mirrors.aliyun.com
Docker Hub 镜像加速(Docker Desktop → Settings → Docker Engine 或 /etc/docker/daemon.json):
{
"registry-mirrors": [
"https://mirror.ccs.tencentyun.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
6.4 你对 Docker 和 Kubernetes 的定位
层次 Docker Kubernetes
─────────────────────────────────────────────────────────────────
职责 单机容器运行时 跨多机容器编排
(创建、启动、停止) (调度、伸缩、自愈)
核心抽象 容器(Container) Pod(一组共享网络/存储的容器)
网络 单机 bridge/overlay CNI 插件,跨节点通信
数据存储 卷(Volume) PV/PVC,支持多种后端
使用场景 开发、测试、简单生产 大规模生产集群
Pod 是什么? Pod 是 K8s 最小的部署单元,一个 Pod 包含一个或多个容器。这些容器共享同一个 Network Namespace(可以用 localhost 通信)和存储卷。Docker 不直接认识 Pod——K8s 通过 CRI(容器运行时接口)调用 Docker(或 containerd)来创建 Pod 里的容器。
一句话:Docker 管单个容器,K8s 管一群容器(通过 Pod 抽象)。
第七层:出问题了 — 排障手册
7.1 先看空间:磁盘去哪了
docker system df
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 15 3 8.2GB 6.1GB (74%)
# Containers 5 2 120MB 80MB (66%)
# Build Cache - - 3.4GB 3.4GB
# 空间浪费常见来源:
# 悬空镜像(dangling):重复 build 产生的无 tag 旧镜像
# 构建缓存:Build Cache 无限增长
# 停止的容器:没加 --rm 参数
docker system prune # 清理未使用的容器/网络/悬空镜像
docker builder prune # 只清理构建缓存
docker system prune -a # 清理所有未使用的镜像
7.2 容器启动失败
docker compose ps # 看状态
docker compose logs app # 看日志
# 常见退出码含义:
# Exit 1 → 应用内部错误
# Exit 137 → 被 OOM Killer 杀了(内存不足)或 kill -9
# Exit 143 → 收到 SIGTERM,正常退出
# 用 shell 替换 CMD,进入容器调试
docker compose run --rm app bash
7.3 构建失败排查
# 看详细构建过程
docker compose build --progress=plain app 2>&1 | tee build.log
# 在失败步骤之前的那一层进入调试
docker history my-app:latest # 找到最后成功的层 ID
docker run --rm -it <layer-id> bash # 在那个层里调试
7.4 网络不通
# 查看网络和容器 IP
docker network ls
docker network inspect blog_internal
# 在容器里测连通性
docker compose exec app ping redis
docker compose exec app curl http://redis:6379
# 看端口映射是否生效
docker compose port app 8000
7.5 性能问题
docker stats # 实时 CPU/内存/IO
docker compose exec app top # 容器内进程
docker inspect blog_app | grep -A5 OOMKilled # 看是否被 OOM 杀死
dmesg | grep -i "killed process" # 宿主机上 OOM 事件
7.6 数据卷问题
docker volume ls
docker volume inspect blog_app_data # 看挂载路径
docker inspect blog_app | grep -A10 Mounts # 容器视角的挂载信息
# 在宿主机直接查看卷内容
docker run --rm -v blog_app_data:/data alpine ls -la /data
附录:常见误解速查表
误解 真相
────────────────────────────────────────────────────────────────────────
"容器是轻量级虚拟机" 容器是隔离的进程,共享宿主机内核
"镜像很大,运行时内存一定也很大" 内存取决于运行的程序,与镜像大小无关
"FROM ubuntu 包含了 Linux 内核" 只有 Ubuntu 用户空间文件,不含内核
"容器必须端口映射才能互访" 同一自定义网络下,容器名直接访问
"depends_on 确保服务就绪" 只控制启动顺序,不等待服务可用
"不挂卷,容器重启后数据还在" 容器删除后可写层消失,数据丢失
"生产环境不需要限制容器资源" 一个失控容器可能耗尽宿主机所有资源
"多个服务用同基础镜像浪费空间" 层共享机制保证同一层只存一份
附录:命令速查
镜像
docker images # 列出本地镜像
docker pull python:3.12-slim # 拉取镜像
docker build -t my-app:v1.0 . # 构建镜像
docker build --no-cache -t my-app . # 强制重建(不使用缓存)
docker rmi my-app:v1.0 # 删除镜像
docker image prune # 清理悬空镜像
docker history my-app:v1.0 # 查看镜像层历史(含每层大小)
容器
docker ps # 运行中的容器
docker ps -a # 所有容器
docker run -d -p 8000:8000 --name blog my-app # 后台启动
docker run --rm -it my-app bash # 临时容器(退出即删)
docker stop blog # 优雅停止(SIGTERM)
docker kill blog # 强制停止(SIGKILL)
docker rm blog # 删除已停止的容器
docker logs -f --tail=100 blog # 最后 100 行日志 + 实时跟踪
docker exec -it blog bash # 进入容器终端
docker exec blog cat /etc/os-release # 在容器内执行单条命令
docker stats # 实时资源使用
docker compose
docker compose up -d --build # 构建并后台启动
docker compose down # 停止并删除容器(保留卷)
docker compose down -v # 停止并删除容器 + 卷(危险)
docker compose ps # 查看服务状态
docker compose logs -f app # 跟踪 app 服务日志
docker compose exec app bash # 进入 app 容器
docker compose restart app # 重启 app 服务
docker compose config # 验证并查看最终合并后的配置
docker compose pull # 拉取最新镜像
清理
docker system df # 查看磁盘占用
docker system prune # 清理未使用资源(不含卷)
docker system prune -a # 清理所有未使用镜像
docker builder prune # 只清理构建缓存
docker volume prune # 清理未使用的卷
本文档基于实战运维经验与内核原理整理,目标是建立一套内在一致、可深可浅的 Docker 知识体系。
如果你对其中某个环节仍有疑问,欢迎继续深入探讨。
还没有评论,来第一个吧