把采集服务装进 Docker:从裸机脚本到可交付镜像的工程实践
采集服务在裸机上跑了半年,没出过大事——直到换服务器那天:Python 版本对不上、cron 任务忘配、字体缺了导致报表乱码,迁移花了一整天。迁完我就把服务容器化了:换环境从「一天」变成「一条命令」。这篇记录容器化过程中的实践和坑。
第一步:写 Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
RUN useradd -r -u 1001 collector \
&& chown -R collector:collector /app
USER collector
CMD ["python", "-m", "app.collect"]
三个细节:slim 基础镜像(够用、体积小)、依赖先于代码拷贝(代码改动不会让依赖层缓存失效)、非 root 运行(安全惯例,数据目录权限一并处理)。
requirements.txt 记得锁版本(requests==2.32.3 这种),否则某天上游更新,镜像构建直接炸——裸机上踩的「依赖漂移」,容器化后并不会自动消失。
第二步:状态外置(最重要的一条)
容器的原则是「可丢弃、可重建」——所以 SQLite 文件、日志、任何有状态的东西,都不能留在容器里:
# docker-compose.yml
services:
collector:
build: .
env_file: .env
volumes:
- ./data:/app/data # serp.db、logs 全在这里
restart: unless-stopped
./data 挂到宿主机上,docker compose down 重建容器,数据纹丝不动。反过来说:如果你发现有文件必须留在容器里才能工作,那是个 bug,迟早会在重建时丢数据。
第三步:调度方式的选择
采集任务大多是「定时批量 + 偶尔长跑」,调度有三种姿势:
| 方式 | 做法 | 适合 |
|---|---|---|
| 容器内 cron | 镜像里装 cron/守护 | 不推荐:日志和退出码都难管 |
| 宿主机 cron + 一次性容器 | docker compose run --rm collector |
批量任务首选 |
| 常驻 worker | 服务自己带调度循环 | 需要常驻并发/队列时 |
我用的第二种,宿主机 crontab 就一行:
0 6 * * * cd /srv/collector && docker compose run --rm collector >> run.log 2>&1
每次跑一个干净容器,跑完即退,退出码就是成败——这比在容器里养一个 cron 守护进程干净得多。
第四步:密钥从环境进,不进镜像
# .env(不进 git)
SERPBASE_API_KEY=xxx
env_file: .env
两个禁忌:别用 ENV/ARG 把 Key 写进 Dockerfile(docker history 能看到镜像层里的值);别把 .env 打进镜像(.dockerignore 里加一行)。
第五步:日志与健康检查
日志:让程序输出到 stdout,宿主机统一收:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
不配上限的话,json-file 日志能把磁盘悄悄吃满——之前磁盘篇讲的坑,容器里换个形式再来一遍。
健康检查:别只测「进程活着」,要测「能力还在」——比如最近一次成功采集的时间:
healthcheck:
test: ["CMD", "python", "-c",
"import app.health, sys; sys.exit(0 if app.health.fresh(240) else 1)"]
interval: 5m
retries: 3
踩坑记录
坑 1:数据留在容器里。 重建容器数据全没。volume 挂载是第一条纪律。
坑 2:时区不对。 容器默认 UTC:日志时间是 UTC、datetime.today() 是 UTC——本地跑没问题、容器里跑出来的「今天」差 8 小时。要么统一按 UTC 思考(存储层推荐),要么显式设 TZ 环境变量,别稀里糊涂。
坑 3:密钥进了镜像层。 ARG API_KEY + ENV API_KEY=$API_KEY 的写法,docker history 一览无余。密钥只走运行时环境。
坑 4:依赖没锁版本。 镜像构建某天突然失败,因为上游发了新版本。requirements.txt 固定到具体版本。
坑 5:root 跑容器。 数据文件权限全归 root,宿主机上想改都费劲。非 root + 目录 chown。
坑 6:迁移完忘了 crontab。 容器化解决了代码和环境,但「谁在几点触发」还是宿主机的事——迁移清单里加上 cron 这一项。
工程清单
- slim 镜像 + 依赖锁版本 + 非 root
- 状态全部外置(volume),容器可随时重建
- 批量任务用「宿主机 cron +
run --rm」,别在容器里养 cron - 密钥走运行时 env,
.dockerignore排除.env - 日志 stdout + 大小上限;healthcheck 测真实能力
- 时区显式声明(UTC 或 TZ),迁移清单含 cron
容器化的价值不在「时髦」,而在把「环境」变成「代码」:过去散落在服务器上的 Python 版本、依赖、字体、cron,现在都在 Dockerfile 和 compose 里,可审查、可重建、可迁移。对一个人的采集项目来说,这是性价比很高的一次工程化。
接口的鉴权方式(Key 走环境变量)在 SerpBase 官方文档 有说明,配合容器化时注意别让密钥进镜像。你们的采集服务跑在裸机还是容器里?迁移时踩过什么坑?评论区聊聊。

浙公网安备 33010602011771号