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 .

拆开看:

那个点是新人最容易忽略、又最容易出问题的地方,所以单独说一句:

它是"构建上下文"——你敲命令时所在的那个目录里的文件,会被整个打包送给构建器。
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 了零遍。

读完你能做什么

下一步:你已经走完了"是什么 → 跑起来 → 装自己的东西"这条线。
最后一章收口:一张常用命令表,和一件更重要的事——什么时候不该用 Docker。

进入 keel 阅读