第 2 周 · D7 · 精讲

Docker
把自己的应用装进集装箱

公司每一个服务,最后都会变成一个镜像、被集群拉起来跑。 今天你自己走一遍这条路 —— 从写 Dockerfile 到把它推进你自己的仓库。

原理 · namespace / cgroup / 分层 构建 · Dockerfile / 多阶段 仓库 · push / pull / 凭据
目标

今天要做出三件可验证的事

第一步
跑起来

浏览器访问你自己电脑上的 localhost:8000,看到你的服务。

第二步
推上去

镜像推进你自己的仓库,换台机器 docker pull 能拉下来。

第三步
改一次

改一行代码重新构建,体会分层缓存带来的秒级重建。

术语第一次听没关系:先做出来 → 再回头理解。今天所有命令都在你自己电脑上跑。

地图

本讲五个段落

  1. 原理 —— 容器到底是什么,为什么比虚拟机轻
  2. 构建 —— 写 Dockerfile,把应用打包成镜像
  3. 运行 —— 跑起来、看日志、进容器排查
  1. 仓库 —— 推送/拉取镜像,以及为什么需要凭据
  2. 优化 —— 分层缓存、多阶段构建、镜像瘦身
  3. 结尾:Docker 的痛 → 为什么要 K8s(下一讲)
概念

三个词,先分清

是什么类比
镜像 image 只读的模板:代码 + 运行环境 + 依赖,一次性打包好 一张安装光盘
容器 container 镜像的一次运行实例,可以起很多个、删了不留痕 光盘装出来的系统
仓库 registry 集中存放镜像的地方,谁都能来拉 应用商店
一句话

镜像不变,容器随时可起可删;仓库负责把镜像送到别的机器上

原理 · 1

容器不是小虚拟机

虚拟机
  • 自带一整套操作系统内核
  • 启动要几十秒
  • 镜像动辄几个 GB
容器
  • 共享宿主机内核,只打包应用与依赖
  • 启动是毫秒级
  • 镜像几十 MB 到几百 MB

所以"轻"的代价是:容器里跑的本质上就是一个被隔离开的普通进程。 隔离开它、限制住它、打包好它 —— 靠的是三样内核能力。

原理 · 2

namespace —— 决定它看得见什么

隔离的几类视角
  • PID 看不到别的进程
  • NET 有自己的网卡/IP
  • MNT 有自己的文件系统
  • UTS 有自己的主机名
  • USER 自己的用户体系
实验:隔离是真是假
# 容器里看到的进程,只有它自己那一个$ docker run --rm alpine ps auxPID   USER     TIME  COMMAND  1   root     0:00  ps aux # 宿主机上看到的却是完整的一大串

“容器里的 1 号进程”和宿主机的 1 号进程,根本不是同一个东西

原理 · 3

cgroup —— 决定它能用多少

限制的是资源
  • CPU 配额(--cpus
  • 内存上限(--memory
  • 还能限 IO、进程数

超了会怎样?直接被杀掉——这就是 K8s 里最常见的 OOMKilled

实验:给 100MB 上限,跑吃内存的程序
$ docker run --rm --memory=100m python:3.12-slim \    python -c "a='x'*(200*1024*1024)"Killed # 容器被内核杀掉,不是程序自己报错# 排查时先看状态:docker inspect 里的 OOMKilled
原理 · 4

分层 —— 镜像为什么改动小、重建快

可写层(容器独有,删了就没)
COPY . .  ← 你的代码
RUN pip install ← 依赖(很少变,可缓存)
WORKDIR /app
FROM python:3.12-slim(基础镜像)
看层
docker history myapp:v1
看改动
docker diff <容器名>

每一层只记录增量。所以:没变的层直接复用,构建就快。

原理 · 小结

一句话记住容器

容器 = 被 namespace 隔开、被 cgroup 限住、 用 分层文件系统 打包好的 —— 一个普通进程

升级到 K8s 之后

这三样能力照样是底座,只是变成了集群帮你管。

所以 K8s 里

限制内存写 resources.limits.memory,超了也是同一个下场。

所以部署时

镜像小、层复用得好,拉取和滚动更新都快。

构建 · 0

镜像名是一段地址

registry.dev.oaiai.ai/你的用户名/hello:v1

仓库地址

镜像存在哪台服务器

项目名

你的命名空间,靠约定自律

名字:标签

版本,别只用 latest

教学仓库

registry.dev.oaiai.ai —— 你们自己的练习田,随你推

生产仓库

registry.akria.net —— 公司线上服务用,新人只读

任务 · 1

写一个 Dockerfile

Dockerfile
# 从哪来
FROM python:3.12-slim
# 工作目录
WORKDIR /app
# 先依赖,后代码(关键顺序)
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
# 启动命令
CMD ["uvicorn","main:app","--host","0.0.0.0","--port","8000"]
FROM

基础镜像,别人的成果

COPY / RUN

构建时执行,每一行就是一层

CMD

容器启动时才执行

顺序有讲究:依赖文件先 COPY,改代码就不会重装依赖。

任务 · 2

构建,然后跑起来

在你自己的电脑上
$ docker build -t hello:v1 .[+] Building 12.4s (9/9) FINISHED $ docker run -d -p 8000:8000 --name hello hello:v1$ curl -s localhost:8000/health{"status":"ok"}
-p 8000:8000

宿主机端口 : 容器端口。左半边是你电脑,右半边是容器。
D2 讲的端口概念,在这里第一次上岗。

-d

后台运行,不加它日志就直接刷屏

任务 · 2.1

出事的时候,靠这四条

命令干什么
docker ps -a看容器在不在、退出码是多少
docker logs -f hello看输出,排障第一件事
docker exec -it hello bash进容器里看文件、试命令
docker inspect hello看完整配置(端口、挂载、OOMKilled

常见坑:容器一起来就退出?多半是 CMD 写的命令前台跑不住(比如写成了 service nginx start 这类后台命令)。容器里的主进程必须待在前台

任务 · 3

推进你自己的仓库

登录 → 改名 → 推送 → 验证
# 1 登录(账号密码课上发放)$ docker login registry.dev.oaiai.ai # 2 给镜像起一个“带仓库地址”的名字$ docker tag hello:v1 registry.dev.oaiai.ai/<你的用户名>/hello:v1 # 3 推上去$ docker push registry.dev.oaiai.ai/<你的用户名>/hello:v1v1: digest: sha256:9f2c… size: 2841

验证方式很重要:先 docker rmi 删掉本地镜像,再 docker pull 拉回来能跑 —— 才算真的推成功了。

任务 · 3.1

凭据不只在推送时需要

你的电脑

构建完 → docker logindocker push

凭据存在 ~/.docker/config.json

集群里的 Pod

要拉你的私有镜像 → 也得有凭据

在 K8s 里这份凭据叫 imagePullSecret

提前记住这个错

ImagePullBackOff —— Pod 起来了但镜像拉不到。十有八九是镜像名写错没给拉取凭据。下一讲会亲手修一次。

串联

同一个镜像名,会在三处出现

① 构建时
FROM

你引用别人的镜像当底座

FROM python:3.12-slim
② 本地编排时
compose

compose 按名字拉起一堆容器

image: postgres:16
③ 部署到集群时
k8s 清单

集群照这个名字去仓库拉

image: registry.dev.oaiai.ai/…

"引用"就是把这条链黏起来的东西:代码 → 镜像 → 仓库 → 集群,全靠镜像名对齐。

任务 · 4

最划算的一行优化:换个顺序

先 COPY 全部代码
COPY . .
RUN pip install -r requirements.txt

改一个字母 → 这层失效 → 依赖全部重装

先依赖,后代码
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

改代码 → 依赖层命中缓存 → 秒级重建

这就是"改一次"的作业要你亲眼看到的东西:看构建输出里那一串 CACHED

任务 · 4.1

多阶段构建:构建环境不进最终镜像

你们天天登的 STARGATE 踏板机,就是这么构建的
# 第一阶段:编译前端(需要 node,但产物只要 dist)
FROM node:22-slim AS web-build
RUN npm ci && npm run build

# 第二阶段:运行时只留 python + 编译好的产物
FROM python:3.11-slim
COPY --from=web-build /app/dist ./web/dist
好处一

最终镜像里没有 node_modules、编译工具链 → 体积可能小一半以上

好处二

攻击面更小:镜像里没有的东西,漏洞也带不进去

任务 · 4.2

两个新手必踩的坑

坑 1Mac 构建出来的镜像集群跑不了

Apple 芯片默认构建 arm64,而集群节点是 amd64,拉过去直接起不来。

docker build --platform linux/amd64 -t hello:v1 .
坑 2把无关文件全 COPY 进去了

本地 .venv/node_modules/.git/ 会一起进镜像,又慢又大。

# .dockerignore
.venv/
node_modules/
.git/

记住:镜像体积是运维成本 —— 拉取、拷贝、滚动更新、备份,每一环都在为它买单。

转折 · 承上启下

单机 Docker,扛不住生产

单机上的痛你只能怎么办
一台机器跑满手工换机器、重新部署一遍
进程挂了半夜爬起来 docker start
要升级版本停服 → 换镜像 → 起回来(客户看得见)
端口冲突手工排端口,还得记住谁占了哪个
要扩到 3 个副本手工起 3 次,再想办法给它们做负载均衡

这些事,K8s 全都替你干了 —— 下一讲一个痛点一个对象地对上。

转折 · 预告下一讲

每个痛点,K8s 都有个东西接

机器跑满
手工换机器

Scheduler / Node
自动挑节点调度

进程挂了
半夜爬起来重启

Deployment
副本掉了自动补

升级要停服
客户看得见

滚动更新
新起旧摘,全程可用

端口冲突 / 对外暴露

Service + Ingress
稳定入口 + 域名路由

配置、密码写死在镜像里

ConfigMap / Secret
配置与镜像分离

随手查

今天最容易卡住的六个地方

现象先试这个
docker login 失败确认账号密码、仓库地址写全(要带域名,不是 IP)
push 403 / denied镜像名少了 <用户名>/ 这一段,或拼错仓库地址
容器起来立刻退出docker logs 看报错;检查 CMD 是不是后台命令
浏览器访问不到-p 没写、端口写反、服务没监听 0.0.0.0
镜像 800MB 起用了完整基础镜像 + 没写 .dockerignore
本地跑得好,集群起不来多半是 --platform 没指定(Mac 芯片)

通用三招:看日志 → 看 inspect → 把报错原文丢给 Claude Code

作业

今天要交的四件事

  1. 写出 Dockerfile,本地构建并跑通(curl 有响应)
  2. 推到你自己的仓库,删本地镜像再拉回来验证
  3. 改一行代码重新构建,把构建输出里的 CACHED 截图
提高题

改成多阶段构建,比较前后镜像体积(docker images

提交

docs/training/学员/<姓名>/D7/,按 D6 的 Git 规范提 PR,提交信息中文

下一讲 · D8

把集装箱交给码头调度

你会拿到自己的集群账号,把今天推上去的那个镜像,部署到集群里, 挂上属于你自己的域名 <你的用户名>.dev.oaiai.ai —— 然后亲手修一次 ImagePullBackOff

注册 STARGATE kubectl 只读 + 自己 ns 可写 GitOps 是什么 部署第一个服务
learn.docker.oaiai.ai D7 · Docker 首页 g
← → 翻页 · f 全屏
/ 1
用 ← → 翻页,按 o 看目录

目录