把 Flask 放进容器,Dockerfile 里有几处写法决定了镜像大小、构建速度、能不能被访问和日志什么时候出现。本机拉不到 python 基础镜像,所以这篇分两部分验证:与基础镜像无关的构建机制用 Docker Desktop 实际构建(FROM scratch);依赖安装、绑定地址、日志缓冲、权限这些则直接在本机用 Flask 和 gunicorn 做同样的事。需要说明:完整的镜像没有构建过,docker run 也没有执行过。
没有 .dockerignore:退出码 0,上下文 52.44MB 有 .dockerignore(venv、.git、__pycache__):退出码 0,上下文 161B 镜像大小:没有 .dockerignore 104.9 MB,有 0.026 MB(COPY . 把 venv 与 .git 都装进了镜像)
前 4 节的构建以 FROM scratch 为基础,验证的是构建机制;后 4 节在本机直接用 Flask 与 gunicorn 验证。
[A:先 COPY .,再 COPY requirements.txt] 只改了 app.py 之后的 COPY 步骤:
COPY . /app built
COPY requirements.txt /app/requirements.txt built
[B:先 COPY requirements.txt,再 COPY app.py] 只改了 app.py 之后的 COPY 步骤:
COPY requirements.txt /app/requirements.txt CACHED
COPY app.py /app/app.py builtshell 形式 CMD gunicorn ... 镜像里的 Cmd = ["/bin/sh","-c","gunicorn -b 0.0.0.0:8000 app:app"] exec 形式 CMD ["gunicorn", ...] 镜像里的 Cmd = ["gunicorn","-b","0.0.0.0:8000","app:app"]
下面是按本篇验证过的规则写的 Flask Dockerfile 的常见形状(这个完整的 Dockerfile 没有被构建过,因为拉不到 python:3.14-slim;每一步的效果在本篇里分别验证过):
FROM python:3.14-slim ENV PYTHONUNBUFFERED=1 WORKDIR /app RUN useradd --create-home app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . USER app EXPOSE 8000 CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]
依赖在源码之前(层缓存)、pip 不留缓存、PYTHONUNBUFFERED、非 root、exec 形式 CMD、绑定 0.0.0.0。
退出码 1;提示:
WARNING: JSONArgsRecommended - https://docs.docker.com/go/dockerfile/rule/json-args-recommended/
JSON arguments recommended for CMD to prevent unintended behavior related to OS signals
WARNING: MaintainerDeprecated - https://docs.docker.com/go/dockerfile/rule/maintainer-deprecated/
Maintainer instruction is deprecated in favor of using label
WARNING: current commit information was not captured by the build: failed to read current commit information with git rev-parse --is-inside-work-treepip install(默认,有缓存) 退出码 0,用时 23.0 s,装了 8 个包,venv 18.8 MB,pip 缓存目录 1.5 MB pip install --no-cache-dir退出码 0,用时 17.7 s,装了 8 个包,venv 18.8 MB,pip 缓存目录 0.0 MB 装好的包: blinker==1.9.0, click==8.5.0, Flask==3.1.3, gunicorn==26.2.0, itsdangerous==2.2.0, Jinja2==3.1.6, MarkupSafe==3.0.3, Werkzeug==3.1.9
-b 0.0.0.0:PORT → 经 127.0.0.1 访问:200 ok;经局域网地址访问:200 ok -b 127.0.0.1:PORT → 经 127.0.0.1 访问:200 ok;经局域网地址访问:ConnectionRefusedError (容器里「外部」访问走的是容器自己的网卡地址,不是容器内的 127.0.0.1;这里的局域网地址是同样的道理,没有用到 Docker 网络)
默认 请求后 1.5 秒日志里有那行:False;进程被 SIGTERM 退出之后有:True PYTHONUNBUFFERED=1 请求后 1.5 秒日志里有那行:True;进程被 SIGTERM 退出之后有:True
目录可写 → 200 written 目录只读(a-w) → 500 PermissionError (这是用权限位模拟,不是真的换了一个用户;root 不受权限位限制,所以容器里用 root 时这个问题看不到)