KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
03 · 把自己的程序装进去 — keel 龙骨
前两章你跑的都是别人做好的镜像。这一章自己做一个。
前两章你跑的都是别人做好的镜像。这一章自己做一个。
一、先有一个小程序
新建一个目录,在里面放一个 app.py。这段代码不需要装任何东西——
用的是 Python 自带的库,这也是我们特意选的:先把流程走通,依赖的事以后再说。
# app.py
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = "这是我在容器里跑起来的程序".encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
先在本机直接跑一下,确认它是活的:
python app.py
然后浏览器打开 http://localhost:8000,能看到那句话。看完按 Ctrl+C 停掉它——
接下来我们要让它跑在容器里,而不是你的机器上。
注意代码里的
0.0.0.0。它表示"接受来自任何地方的连接"。
如果你写成127.0.0.1,程序就只能被它自己所在的机器访问——
放进容器后,外面的端口映射就递不进去了。这个细节后面还会再遇到。
二、写一个 Dockerfile
同目录下新建一个文件,名字就叫 Dockerfile(没有后缀名):
FROM python:3.12-alpine
WORKDIR /app
COPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]
五行,逐个读:
| 行 | 在说什么 |
|---|---|
FROM python:3.12-alpine |
从哪个镜像开始。我们不从零造,而是在一个已经装好 Python 的现成镜像上加东西。alpine 是这类镜像的一个"精简版"变体,体积小 |
WORKDIR /app |
进到容器里的 /app 目录。后面几行的相对路径都相对它 |
COPY app.py . |
把你本机的 app.py 复制到容器里的当前目录(也就是 /app) |
EXPOSE 8000 |
声明"这个程序用 8000 端口"。它只是一句声明,不会真的开放端口——真正把端口递出去的是运行时的 -p |
CMD ["python", "app.py"] |
容器跑起来之后要执行的那条命令 |
Dockerfile 不是脚本。 它不会被"执行",它是给构建器读的说明书:
读完之后产出一个镜像。这就是为什么下一节命令叫build,而不是run。
三、构建成镜像
在你放这两个文件的目录下敲:
docker build -t myapp .
拆开看:
docker build:造一个镜像出来(不是跑);-t myapp:给造出来的镜像起个名字叫myapp(-t是 tag 的意思);- 最后的
.:一个点,表示"当前目录"。
那个点是新人最容易忽略、又最容易出问题的地方,所以单独说一句:
它是"构建上下文"——你敲命令时所在的那个目录里的文件,会被整个打包送给构建器。
Dockerfile 里的COPY只能从这个范围里拿文件,拿不到外面的。
现在你目录里只有两个文件,所以无所谓;等你目录里有node_modules、.git、
几十兆的日志时,这个点就会开始影响构建速度和镜像体积。
(想知道它到底是怎么回事:《Docker 概念课 · 构建上下文》。现在不需要点。)
成功的话,最后几行会看到类似:
=> [1/3] FROM docker.io/library/python:3.12-alpine
=> [2/3] WORKDIR /app
=> [3/3] COPY app.py .
=> exporting to image
=> => naming to docker.io/library/myapp
⚠️ 未实测:本机 daemon 未运行,以上按标准形状写成。你的行数、步骤编号会不同。
有个细节值得留意:WORKDIR、COPY 都各自是一步。这说明 Docker 是"一步一步"把镜像叠出来的
——为什么它会这样、这对你有什么影响(改一行代码会不会导致重装依赖),
是《Docker 应用课》里一整章的内容。
确认镜像造出来了:
docker images
列表里应该有一行,REPOSITORY 是 myapp,TAG 是 latest。
四、跑起来
docker run -d --name myapp -p 8000:8000 myapp
和上一章的 nginx 是同一种读法,只是把镜像名从 nginx 换成了 myapp,
端口换成 8000(因为我们的程序监听的是 8000)。
浏览器打开 http://localhost:8000——你又看到那句话了。
但这一次不一样:这句话不是 Python 在你机器上跑出来的,
是你刚才 docker build 造的那张"光盘",在容器里跑出来的。
验证一下:
docker ps # 应该看到 myapp 在运行
docker logs myapp # 应该看到 HTTP 请求的日志
docker stop myapp
docker rm myapp
五、它不是什么
这一章最容易混的是这两条命令:
docker build和docker run是两件事。
build= 把说明书(Dockerfile)读一遍,造出一张"光盘"(镜像)。造完就结束了。run= 拿一张"光盘",播放一次(容器)。所以:改了
app.py之后,只run是不会生效的——你跑的还是旧镜像。
必须重新build,再造一张新的,然后run新的。
这也是很多人第一次改代码"改了没反应"的原因:他 run 了三遍,build 了零遍。
读完你能做什么
- 写一个 5 行的 Dockerfile,把自己的程序装进去、构建成镜像、再跑成容器;
- 说出 Dockerfile 里每一行的作用,以及
EXPOSE和-p的分工; - 说清"改了代码为什么要重新 build"。
下一步:你已经走完了"是什么 → 跑起来 → 装自己的东西"这条线。
最后一章收口:一张常用命令表,和一件更重要的事——什么时候不该用 Docker。