🧪 第15章 部署物件侦测应用
📄 一句话概括

先把项目和区域定下来,后面每条命令都用这两个默认值。 Dockerfile 九行,只用了七种指令;EXPOSE 是声明,不是真的开端口。 build 失败先分清两类:Dockerfile 写错了,还是 Docker 自己没起来。

📝 本章和前面几章的关系

这一篇对应教材 15.5 到 15.7.1:configuration 初始设定、制作 Dockerfile、建置 Docker 映像档、 确认建置的映像档。 上一篇讲清了约束,这一篇把镜像真写出来——整章唯一能落到文件上的东西,就是那九行。

一、步骤 1:把项目和区域定下来

gcloud config set project my-project-id
gcloud config set run/region asia-east1

教材 15.5。项目 ID 和区域定完,后面所有命令都用这两个默认值,不用每条都带参数。 项目 ID 不是项目名。它全局唯一,别人占了你就不能用,而且定了改不了。 区域要选近的。asia-east1 是台湾,asia-northeast1 是东京。选远了延迟高,跨境流量还可能另有计费。

这两条 config 的作用是省掉后面每条命令的 --project 与 --region。 忘了设的后果不是报错,是用错项目——资源建到别处去了,账单也跟着跑。 所以部署完第一件事是回头看一眼 gcloud config list。

二、步骤 2:Dockerfile 九行

FROM python:3.13-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY detect_api ./detect_api
COPY wsgi.py .
EXPOSE 8080
CMD ["gunicorn", "-b", ":8080", "-w", "2", "wsgi:app"]
指令 出现几次 干什么
FROM 1 基础镜像,自带 Python 3.13 的瘦身版
WORKDIR 1 定工作目录,之后的相对路径都相对它
ENV 1 两个环境变量,见下一节
COPY 3 先拷依赖清单,再拷应用包与入口
RUN 1 装依赖,这一层最慢
EXPOSE 1 声明端口,只是文档
CMD 1 容器启动时跑这条

九行只用了七种指令。Dockerfile 就那么几个词,不用背,查一次就够。 EXPOSE 是个声明,不是开关。它不真的开放端口,只告诉读文件的人(和平台)「这个镜像打算用 8080」。 真正决定能不能连的,是进程有没有 listen。 --no-cache-dir 是给 pip 的:不把下载的 wheel 留在本地。镜像里那点缓存一点用没有,只会让层变大。

gunicorn 那两个参数:-b :8080 绑端口,-w 2 起两个工作进程。 这才是「容器里只有一个进程」的正解——一个主进程管两个 worker, 而不是在容器里跑 systemd 拉起一堆东西。 为什么是 gunicorn 不是 flask run:后者是开发服务器,单进程、无并发,官方文档明确写了不能上生产。 第 1 章用它是因为它带自动重载和调试页。

三、那两行 ENV 是干什么的

变量 作用 不开会怎样
PYTHONUNBUFFERED=1 print 不缓冲,直接输出 日志卡住,看不到实时输出
PYTHONDONTWRITEBYTECODE=1 不生成 .pyc 镜像里多一堆缓存文件

第一行比第二行重要得多。 容器的日志是 stdout 收走的,Python 默认会缓冲,结果就是「程序明明在跑,日志一条没有」。 这个坑很典型:本机跑的时候终端是 tty,自动行缓冲,看不出问题;一进容器就变成全缓冲。 同一份代码两种行为,差别只在有没有终端。 第二行只是省空间,不影响行为。 两行写进同一条 ENV,产生一层不是两层——这是上一篇那个「层」的概念第一次派上用场。

四、.dockerignore:不写会打进去什么

.git
__pycache__
*.pyc
.venv
instance/
tests/
htmlcov/
.pytest_cache/
*.log
目录 会被拷进镜像吗 实测大小
detect_api/ 会(要拷) 2 个文件,0.0 KB
__pycache__/ 不会(挡住了) 1 个文件,4.0 KB
htmlcov/ 不会 8.0 KB

不写 .dockerignore 的后果:COPY . . 会把这些全拷进去。 __pycache__ 是上一台机器编的字节码,进了容器可能是错的版本; htmlcov 是覆盖率报告,真项目里几十兆;.git 可能有几百兆。 还有一个更隐蔽的:instance/ 里有你的 SQLite 库和上传的图片。 打进镜像等于把测试数据发布出去了,而且下次部署还会覆盖线上的。 顺序上,.dockerignore 要在 docker build 之前就位——它是在发送构建上下文那一刻生效的, 镜像已经开始构建就晚了。

五、步骤 3:构建,以及两类失败

docker build -t detect-api:v1 .
报的什么错 说明什么 往哪查
未知指令、语法错 Dockerfile 写错了 看那九行
找不到文件 COPY 的源路径不对 看构建上下文与 .dockerignore
连不上 daemon Docker 本身没起 去启动 Docker Desktop

本沙箱实测返回码 1,报的是第三种: failed to connect to the docker API at npipe:////./pipe/dockerDesktopLinuxEngine。 跟 Dockerfile 一点关系都没有。 这一点值得记:build 失败,先分清是「Dockerfile 写错了」还是「Docker 自己没起来」。 两类错往两个方向查,混在一起会白折腾半天。 真跑起来之后,输出是一长串 => [1/6] FROM ...、=> [2/6] COPY ...,每条对应一行。 那串东西就是「分层」的可视化,也是上一篇那张表的实物。

结尾会有镜像 ID,docker images 里能看到 detect-api:v1 那一行, SIZE 一列就是镜像多大——上一篇算不出来的那个数,到这一步才有个真答案。

六、标签怎么起

写法 含义
detect-api:v1 本地仓库名加标签
gcr.io/my-project/detect-api:v1 推到 GCR 时的完整名
latest 默认标签,别在生产用

latest 的问题很具体:它不指向任何确定的东西。昨天 latest 是 v1,今天推了 v2 还是 latest。 出了问题要回滚,你不知道该回到哪个。 用版本号或者 git 短哈希,回滚才有据可依。 顺带说清一件事:镜像名加标签,只是给同一堆层挂个名字,改名不花时间也不复制。 下面推 GCR 那一步就靠这个性质。

七、动手 Demo:步骤 1~3:配置、Dockerfile、构建

这个 Demo 把九行 Dockerfile 逐行解析,再验 .dockerignore 到底挡住了什么,最后真跑一次 build。 第五节那个返回码是 1,原因和 Dockerfile 无关——正文专门用一节分开了这两类失败。

FROM python:3.13-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY detect_api ./detect_api
COPY wsgi.py .
EXPOSE 8080
CMD ["gunicorn", "-b", ":8080", "-w", "2", "wsgi:app"]

实跑输出:

--------------------------------------------------------------------
步骤 1~3:配置、Dockerfile、构建
--------------------------------------------------------------------

  ① 步骤 1:把项目定下来
  --------------------------------------------------------------------

  [平台原样 · 本沙箱不执行]
  | gcloud config set project my-project-id
  | gcloud config set run/region asia-east1
  教材 15.5。项目 ID 和区域定完,后面所有命令都用这两个默认值。

  ! 区域要选近的。asia-east1 是台湾,asia-northeast1 是东京。
  选远了延迟高,而且跨境流量可能另有计费。
  ! 项目 ID 不是项目名。它是全局唯一的,别人占了你就不能用。

  ② 步骤 2:Dockerfile
  --------------------------------------------------------------------

  [文件原样]
  | FROM python:3.13-slim
  | WORKDIR /app
  | ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
  | COPY requirements.txt .
  | RUN pip install --no-cache-dir -r requirements.txt
  | COPY detect_api ./detect_api
  | COPY wsgi.py .
  | EXPOSE 8080
  | CMD ["gunicorn", "-b", ":8080", "-w", "2", "wsgi:app"]
  九行,是这一章唯一能「真的写出来」的东西。

  逐行解析 -> 共 9 条指令
    FROM   python:3.13-slim
    WORKDIR /app
    ENV    PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
    COPY   requirements.txt .
    RUN    pip install --no-cache-dir -r requirements.txt
    COPY   detect_api ./detect_api
    COPY   wsgi.py .
    EXPOSE 8080
    CMD    ["gunicorn", "-b", ":8080", "-w", "2", "wsgi:app"]
  ! 九行里只用了 7 种指令:FROM、WORKDIR、ENV、COPY、RUN、EXPOSE、CMD。
  Dockerfile 就那么几个词,不用背,查一次就够。

  ③ 那两行 ENV 是干什么的
  --------------------------------------------------------------------

| 变量 | 作用 | 不开会怎样 |
|---|---|---|
| `PYTHONUNBUFFERED=1` | print 不缓冲,直接输出 | 日志卡住,看不到实时输出 |
| `PYTHONDONTWRITEBYTECODE=1` | 不生成 .pyc | 镜像里多一堆缓存文件 |

  ! 第一行比第二行重要得多。
  容器的日志是 stdout 收走的,Python 默认会缓冲,结果就是「程序明明在跑,日志一条没有」。
  ! 这个坑很典型:本机跑的时候终端是 tty,自动行缓冲,看不出问题;一进容器就变成全缓冲。
  同一个代码两种行为,差别只在有没有终端。

  ④ .dockerignore 与「会打进去什么」
  --------------------------------------------------------------------
  .dockerignore -> 9 条
    .git
    __pycache__
    *.pyc
    .venv
    instance/
    tests/
    htmlcov/
    .pytest_cache/
    *.log
  会进镜像的 detect_api/ -> 2 个文件,0.0 KB
  被 ignore 的 __pycache__/ -> 1 个文件,4.0 KB
  被 ignore 的 htmlcov/ -> 8.0 KB
  ! 不写 .dockerignore 的后果:COPY . . 会把这些全拷进去。
  __pycache__ 是上一台机器编的字节码,进了容器可能是错的版本;htmlcov 是覆盖率报告,几十兆;
  .git 可能有几百兆。
  ! 还有一个更隐蔽的:instance/ 里有你的 SQLite 库和上传的图片。
  打进镜像等于把测试数据发布出去了,而且下次部署会覆盖线上的。

  ⑤ 步骤 3:构建(这一步在本沙箱跑不了)
  --------------------------------------------------------------------
  docker build -t detect-api:v1 . -> 返回码 1
  ERROR: failed to connect to the docker API at npipe:////./pipe/dockerDesktopLinuxEngine;
  ! 跑不了,原因还是那个:daemon 没起。注意报的错跟 Dockerfile 一点关系都没有。
  ! 这一点值得记:docker build 失败的时候,
  先分清是「Dockerfile 写错了」还是「Docker 自己没起来」。前者报的是语法错、找不到文件;
  后者报的是连不上 daemon。两类错要往两个方向查。
  ! 真跑起来之后,输出会是一长串 => [1/6] FROM ...,每一条对应 Dockerfile 里一行。
  那串东西就是「分层」的可视化。

  ⑥ 镜像标签
  --------------------------------------------------------------------

| 写法 | 含义 |
|---|---|
| `detect-api:v1` | 仓库名加标签 |
| `gcr.io/my-project/detect-api:v1` | 推到 GCR 时的完整名 |
| `latest` | 默认标签,**别在生产用** |

  ! latest 的问题很具体:它不指向任何确定的东西。昨天 latest 是 v1,今天推了 v2 还是 latest。
  出了问题回滚,你不知道该回到哪个。
  ! 用版本号或者 git 短哈希,回滚才有据可依。

第三节 detect_api/ 只有 2 个文件,因为这是第 14 章造的最小骨架。 第四节 htmlcov 8.0 KB 是刚跑完覆盖率留下的,真项目里它几十兆。 第五节返回码 1 报的是连不上 daemon,跟那九行一个字都没关系。

30 秒实验

  1. 把三条 COPY 换成一句 COPY . .,再看 .dockerignore 还需要挡哪些东西。
  2. 把 CMD 里的 -w 2 改成 -w 4,想一想内存占用会不会翻倍。
  3. 想一想:把 EXPOSE 8080 删掉,容器还能不能从 8080 访问?

常见误区

  1. 先 COPY . . 再 pip install:改一行代码就要重装全部依赖。
  2. 不写 .dockerignore:把 .git、instance/、htmlcov 一起打进镜像。
  3. 拿 flask run 当启动命令:开发服务器不能上生产,单进程无并发。
  4. 标签只用 latest:出问题回滚时不知道回哪个。
  5. build 失败就往 Dockerfile 里找:可能是 daemon 根本没起。
自测三问(点开对答案) 1. 九行 Dockerfile 用了哪几种指令? —— FROM、WORKDIR、ENV、COPY、RUN、EXPOSE、CMD,七种。 2. EXPOSE 是真开端口吗? —— 不是,只是声明;能不能连看进程有没有 listen。 3. build 报「连不上 daemon」说明什么? —— 跟 Dockerfile 无关,是 Docker 服务没启动。

镜像建好了,下一篇把它推上去、部署,然后拿六条请求逐项验收。

相关笔记