先把项目和区域定下来,后面每条命令都用这两个默认值。 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 秒实验
- 把三条 COPY 换成一句
COPY . .,再看 .dockerignore 还需要挡哪些东西。 - 把
CMD里的-w 2改成-w 4,想一想内存占用会不会翻倍。 - 想一想:把
EXPOSE 8080删掉,容器还能不能从 8080 访问?
常见误区
- 先 COPY . . 再 pip install:改一行代码就要重装全部依赖。
- 不写 .dockerignore:把 .git、instance/、htmlcov 一起打进镜像。
- 拿 flask run 当启动命令:开发服务器不能上生产,单进程无并发。
- 标签只用 latest:出问题回滚时不知道回哪个。
- build 失败就往 Dockerfile 里找:可能是 daemon 根本没起。
自测三问(点开对答案)
1. 九行 Dockerfile 用了哪几种指令? —— FROM、WORKDIR、ENV、COPY、RUN、EXPOSE、CMD,七种。 2. EXPOSE 是真开端口吗? —— 不是,只是声明;能不能连看进程有没有 listen。 3. build 报「连不上 daemon」说明什么? —— 跟 Dockerfile 无关,是 Docker 服务没启动。镜像建好了,下一篇把它推上去、部署,然后拿六条请求逐项验收。
- ☐ (本篇完,下一篇 15.4 步骤 4~5:推上 GCR、部署到 Cloud Run)