把 Python 定时脚本装进 Docker:一份生产级 Dockerfile 与七个踩坑点

举报
yd_254324137 发表于 2026/10/24 11:16:55 2026/10/24
【摘要】 依赖漂移、时区错乱、日志空白、SIGTERM 不掉——把 Python 定时脚本容器化时最容易踩的七个坑,附一份可直接套用的 Dockerfile 与 compose 模板。

为什么要容器化一个定时脚本

一个 Python 定时脚本从「今天能跑」到「半年后还能跑」,中间隔着的不是功能,而是环境。服务器换了一台、系统 Python 从 3.9 升到 3.12、某天 pip install 装到了新版本依赖、日志写满磁盘把机器拖死——这些都是我真实遇到过的问题。

容器化能一次性解决其中大部分:镜像把依赖钉死,时区和运行用户随镜像走,日志统一到 stdout,重启策略交给 Docker。下面这份模板是我在几台机器上反复改出来的,可以直接套用。

最小可用 Dockerfile

FROM python:3.12-slim

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    TZ=Asia/Shanghai \
    PIP_NO_CACHE_DIR=1

RUN apt-get update && apt-get install -y --no-install-recommends tzdata tini \
 && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone \
 && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

RUN useradd -m -u 1000 app && chown -R app:app /app
USER app

ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["python", "-m", "job.main"]

看着不长,但每一行都在填一个坑。

坑点一:设了 TZ 环境变量,时间还是差八小时

ENV TZ=Asia/Shanghai 只影响调用 localtime 的程序,而 slim 镜像里根本没有时区数据库。必须装 tzdata 并把 /etc/localtime 指过去,两者缺一不可。

排查方法很简单:进容器执行 date,如果还是 UTC,就是只设了环境变量没设系统时区。

坑点二:docker logs 一片空白

Python 默认按块缓冲 stdout,任务跑完或者缓冲区满才输出。定时脚本往往几分钟就退出,结果日志一行都看不到。加 PYTHONUNBUFFERED=1 解决。

同时建议日志只走 stdout,不要自己再写文件:

import logging, sys
logging.basicConfig(
    level=logging.INFO,
    stream=sys.stdout,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
)

落盘交给 Docker 的日志驱动,配合 max-size / max-file 做轮转,比自己在代码里切分文件省事得多。

坑点三:容器停不下来,要等 10 秒超时被强杀

容器里的 1 号进程负责转发信号。如果你的启动命令是 shell 形式(CMD python job.py),1 号进程是 /bin/sh,它不会把 SIGTERM 传给 Python,Docker 等 10 秒后直接 SIGKILL,正在写的文件就断在半路。

两个修法:用 exec 形式(JSON 数组)启动,或者干脆加 tini 做 1 号进程,它还会顺手回收僵尸进程。

坑点四:切成非 root 后,挂载目录写不进去

镜像里用 uid 1000 的 app 用户,宿主机的 ./data 往往属于 root 或其他 uid,一写就 Permission denied。两种处理:

  • 宿主机上执行 chown -R 1000:1000 ./data
  • 或者构建时把 uid 做成构建参数,按宿主机的实际 uid 生成

不建议为了省事直接 USER root,一旦脚本被利用,root 容器基本等于把宿主机交出去。

坑点五:三个月后重新构建,装出来的是另一个程序

requirements.txt 里写 requests 和写 requests==2.32.3,是两种命运。生产镜像一定要锁死版本,最好再加一份 constraints.txt 管住间接依赖。

另外两点:PIP_NO_CACHE_DIR=1 能让镜像小几十 MB;--no-install-recommends 能少装一堆用不到的包。

坑点六:进程活着,不等于任务成功

restart: unless-stopped 只管进程退出。定时脚本更常见的失败是「进程还在、但每轮都抛异常」。这时健康检查要检查业务状态,而不是端口:

HEALTHCHECK --interval=5m --timeout=10s --start-period=2m \
  CMD python -c "import sys,time,pathlib; p=pathlib.Path('/app/data/last_ok'); sys.exit(0 if time.time()-p.stat().st_mtime < 7200 else 1)"

再配合一条简单的告警,比事后翻日志高效得多。

坑点七:定时到底放容器里还是放宿主机

两种做法我都用过:

  • **宿主机 cron + docker exec**:调度直观,但容器重建后 exec 可能打到不存在的容器上。
  • **容器内 APScheduler 常驻**:调度跟着镜像走,重启后自动恢复;代价是要处理「同一时刻只跑一份」的问题。

如果只跑单实例,容器内调度更省心。一旦要多副本,就必须加分布式锁(Redis 的 SET NX PX 就够用),否则同一分钟会跑出好几份结果,把下游数据写花。

一份可以直接用的 compose

services:
  job:
    build: .
    restart: unless-stopped
    environment:
      - TZ=Asia/Shanghai
      - LOG_LEVEL=INFO
    volumes:
      - ./data:/app/data
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

小结

把上面的内容压成一张检查表:镜像里装 tzdata 且设置 /etc/localtime;PYTHONUNBUFFERED=1;exec 形式启动或加 tini;非 root 运行并处理好卷权限;依赖锁版本;日志走 stdout 并配置轮转;健康检查检查业务状态;多副本必须加锁。

做完这八件事,脚本才算真的「放上去不用管」。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。