项目标题写的是Ariflow我基本可以确定这里想说的是Apache Airflow——数据工作流编排领域绕不开的调度平台项目文档里这种手滑写法我见过不止一次后文统一按Airflow来。今天写这篇文章不是教大家怎么编排DAG而是完整复盘一遍怎么把一套生产可用的Airflow环境固化成Docker镜像。从基础镜像选型、依赖版本锁定到国内网络下的镜像加速、离线交付、往内网仓库推送尽量把每个决定背后的原因也讲清楚。我要直接说一个结论构建Airflow镜像这件事“能用”和“生产可用”之间差着一整条坑道。很多人觉得就是拿来官方Dockerfile改两行然后docker build真上手就会发现基础镜像拉不下来、pip解析依赖冲突、provider版本对不上、容器起来后scheduler不干活、时区还是UTC这些问题每一个都能消耗半天。这篇文章适合两类人一是负责公司数据平台交付、需要把Airflow镜像打进内网仓库的运维或平台工程师二是想把自己的Airflow部署固化成标准产物的开发同学。读完你会拿到一份可以直接抄的Dockerfile外加一份排错对照。1. 放着现成镜像不用为什么还要自己build先回答一个很多人没想清楚的问题官方明明提供了apache/airflow镜像为什么还要自己构建1.1 官方镜像能做什么缺什么Apache Airflow官方在Docker Hub上发布 apache/airflow 镜像里面已经封装了完整的运行环境airflow用户、scheduler/webserver/worker的启动入口、默认的airflow.cfg、日志配置以及一部分常用依赖。配合docker compose起一个标准Airflow环境并不费劲对于学习和功能验证完全够用。但它的问题也很明显官方镜像是一个“通用产物”不可能预装你的业务依赖。我在实际项目里列过一张缺失清单大概包括四类。第一类是Python第三方库比如 pymysql、psycopg2-binary、requests、openpyxl、pandas 这些业务DAG一跑起来几乎都要。第二类是Airflow provider官方镜像只带少量基础provider像钉钉/企业微信通知、Spark、Snowflake、KubernetesExecutor 这些都需要单独装。第三类是公司内部的Python包通常没有上传到公网PyPI只在内部仓库或代码库里。第四类是DAG和plugins本身开发环境可以挂载但生产环境更希望把它们固化在镜像里。1.2 哪些场景值得自建镜像基于上面的缺失清单我归纳了四个值得自建镜像的场景。一是内网或离线环境交付。很多公司的服务器根本访问不了外网如果还依赖部署时现场拉镜像、现场pip install基本等于宣判失败。提前在有网环境把所有东西构建进镜像再推到内网Harbor或做离线导出这才是可持续的路径。二是多环境一致性。开发、测试、生产共用同一个镜像产物能直接干掉“我本地能跑服务器上不行”这类问题。镜像成了不可变交付物DAG代码、依赖版本、系统配置在三个环境完全一致。三是安全与合规基线。自建镜像可以主动控制基础镜像的版本、去掉多余的调试组件、固定非root用户运行、清理构建缓存这些在安全审计时都是实打实的加分项。四是平台化批量交付。如果团队要同时维护多个项目或租户的Airflow实例一个基础镜像加上不同DAG产物可以在一套CI里批量产出避免每个实例从零开始配置。1.3 什么时候别自己折腾反过来也要泼一点冷水。如果你只是刚接触Airflow、在笔记本上跑个demo直接用官方镜像配合挂载DAG就够了没必要一上来就折腾镜像。团队如果没有CI/CD流水线和镜像仓库手动构建的维护负担反而比直接部署更大。另一个常见误区是过度定制在Dockerfile里顺手打补丁、装一堆系统工具最后镜像变得又大又难维护。自建的目的是可控结果反而搞出新的不可控那就得不偿失了。2. 动手前先定三件事基础镜像、版本组合和依赖来源很多人踩坑的起点不是Dockerfile写错而是动手之前什么都没定。构建Airflow镜像前我建议先把三件事敲定。2.1 基础镜像选型官方镜像和自建Python环境怎么选第一件事是选基础镜像。绝大多数团队适合直接基于 apache/airflow 官方镜像扩展而不是从 python:slim 重新搭。原因在于官方镜像已经把最绕的部分处理好了airflow用户的权限模型、启动入口脚本、日志配置、部分系统依赖这些自己手动做要花很多精力而且官方升级后你自己的旋钮往往跟不上。对比维度apache/airflow 官方镜像python:3.11-slim 自建Airflow核心已内置开箱即用需自己pip安装并处理约束系统用户与权限内置airflow用户需useradd并配sudo启动与初始化官方entrypoint已处理需自己写webserver/scheduler入口镜像体积较大可以更小维护成本低跟随上游高所有问题自己扛适用场景生产环境首选对体积或系统层有特殊要求如果你对镜像体积特别敏感或者要在基础层装大量系统库再从python镜像自建。但我必须提醒这条路的核心问题不是装不上Airflow而是后续的入口脚本、权限处理、健康检查都要自己补维护成本不低。2.2 版本锁法Airflow、Python、Provider三张牌第二件事是版本组合。我这里给出一个目前我用的组合Airflow 2.10.3 Python 3.11所有第三方依赖用官方constraints文件锁死。为什么一定要用constraints因为Airflow的依赖非常敏感。pip的resolver默认是“严格回溯”的它为了让新装包满足依赖会把环境中已有的SQLAlchemy、Werkzeug、Flask等底层库自动升级而这些底层库一旦被升到不兼容版本Airflow启动时就会在import阶段直接崩掉。官方constraints文件就是把这一堆依赖的版本号全部钉死安装时通过-c传进去pip就不会乱动了。constraints文件的地址格式大概是这样https://raw.githubusercontent.com/apache/airflow/constraints-2.10.3/constraints-3.11.txt注意中间的分支名要和Airflow大版本对应文件名里的3.11换成你实际使用的Python主版本号。如果你用的是2.9.2就把分支改成 constraints-2.9.2不能混。2.3 依赖清单三类放一起还是分开第三件事是依赖清单。我的经验是不要把几十个依赖堆在一个requirements.txt里至少按三类拆分一类是DAG运行时必备库比如requests、pandas、pymysql一类是provider库比如apache-airflow-providers-postgres、apache-airflow-providers-http一类是内部私有包。拆分的好处是改动任何一类Docker层缓存只失效对应的那一段构建速度会快很多。私有包如果发布在公司内部PyPI上可以在requirements里直接写--extra-index-url指向内网源如果没有内部PyPI就把wheel文件放进构建上下文Dockerfile里用pip install /tmp/packages/*.whl安装。我倾向于用内部PyPI因为它能把镜像和构建机解耦。3. 一份能直接落地的Dockerfile逐行拆给你看这部分直接给方案。以下是我最近使用的一个Dockerfile基于官方镜像扩展适合大多数生产场景ARG AIRFLOW_VERSION2.10.3 FROM apache/airflow:${AIRFLOW_VERSION} ARG PIP_INDEX_URLhttps://pypi.tuna.tsinghua.edu.cn/simple ARG CONSTRAINTS_URLhttps://raw.githubusercontent.com/apache/airflow/constraints-2.10.3/constraints-3.11.txt ENV AIRFLOW_HOME/opt/airflow \ TZAsia/Shanghai COPY requirements.txt /tmp/requirements.txt USER root RUN pip install --no-cache-dir \ -r /tmp/requirements.txt \ -c ${CONSTRAINTS_URL} \ -i ${PIP_INDEX_URL} \ apt-get clean \ rm -rf /tmp/requirements.txt COPY --chownairflow:airflow dags/ /opt/airflow/dags/ COPY --chownairflow:airflow plugins/ /opt/airflow/plugins/ COPY --chownairflow:airflow config/airflow.cfg /opt/airflow/airflow.cfg USER airflow WORKDIR ${AIRFLOW_HOME}第一行ARG写在FROM之前这是Docker唯一允许在决定基础镜像阶段使用的ARG值用来把Airflow版本参数化。构建时执行docker build --build-arg AIRFLOW_VERSION2.10.3 .就能切换版本不需要改文件。中间切到root再pip install是因为官方镜像里airflow用户对/usr/local的写权限有限不切换到root安装第三方包很可能直接失败。装完之后用apt-get clean清理apt缓存顺手删掉临时requirements尽量减小镜像体积。紧接着把DAG、插件、配置文件拷进镜像COPY使用了--chownairflow:airflow。这一步容易忽略如果漏掉文件owner会变成root容器以airflow用户启动后可能没有日志写入权限调度任务时会出现权限报错。最后切回airflow用户并设置工作目录。这样容器最终进程不是root符合安全基线。3.1 追求极致体积从python:slim自建的思路如果你对体积和定制化要求很高可以从python:3.11-slim起步。核心思路是先建一个airflow系统用户创建/opt/airflow作为AIRFLOW_HOME然后安装apache-airflow本体和requirements最后把DAG和插件拷贝进去。FROM python:3.11-slim ENV AIRFLOW_HOME/opt/airflow RUN useradd -m -u 50000 airflow \ mkdir -p ${AIRFLOW_HOME} \ chown -R airflow:airflow ${AIRFLOW_HOME} COPY requirements.txt constraints.txt /tmp/ RUN pip install --no-cache-dir \ apache-airflow2.10.3 \ -r /tmp/requirements.txt \ -c /tmp/constraints.txt COPY --chownairflow:airflow dags/ /opt/airflow/dags/ COPY --chownairflow:airflow plugins/ /opt/airflow/plugins/ USER airflow WORKDIR ${AIRFLOW_HOME}但要注意这种方案下官方入口脚本的所有便利都没了你需要在部署时自己处理数据库初始化、管理员账号创建、scheduler和webserver进程的启动命令。我的建议是除非团队真的对镜像体积或者系统层有硬性要求否则不要为了“更干净”选这条路。镜像体积省下来几百MB维护成本往往翻倍。3.2 .dockerignore构建上下文瘦身.dockerignore经常被忽略但它直接影响构建速度。一个完整仓库如果直接把dags插件目录下几万行代码、日志、缓存文件全发到构建上下文每次build都会很慢缓存命中也会变差。我的建议至少包含这些.git .gitignore *.pyc __pycache__/ .venv/ venv/ .env logs/ airflow_home/ *.db .DS_Store这里有个细节.dockerignore不是Dockerfile里的指令它的作用范围是构建上下文也就是docker build命令最后这个点所指向的目录。写好之后用docker build -t airflow-custom ... .时海量垃圾文件就不会被一股脑塞给守护进程了。4. 国内网络下构建Airflow镜像的加速三板斧这一节应该是最多人需要的。Airflow镜像构建最常见的三个痛点Docker Hub拉不动、PyPI下载慢、模型或大文件依赖不知道去哪下。逐个解决。4.1 Docker守护进程配置镜像加速Docker镜像拉取慢是国内环境的第一道坎。桌面版Docker或Linux下可以修改/etc/docker/daemon.json添加registry-mirrors配置{ registry-mirrors: [ https://docker.m.daocloud.io, https://hub-mirror.c.163.com ] }改完后执行sudo systemctl restart docker桌面版在设置里重启Docker Engine即可。这里必须多说一句网络上流传的加速器地址变动很频繁很多公开mirror今天能用明天就挂以上地址也要以各家官方页面为准。如果你所在公司已经有内网Harbor或Nexus最好在里面建一个docker代理仓库把 apache/airflow 等基础镜像拉下来同步这样构建机的加速器指向内网代理就行稳定且不依赖外网状态。4.2 pip源参数化而不是依赖镜像里的pip.conf第二道坎是pip下载慢。官方PyPI在国内访问经常是几十KB每秒构建时在Dockerfile里把-i指向国内源就行。Dockerfile里我已经暴露了ARG PIP_INDEX_URL构建时用--build-arg覆盖docker build -t airflow-custom:2.10.3 \ --build-arg PIP_INDEX_URLhttps://pypi.tuna.tsinghua.edu.cn/simple \ --build-arg CONSTRAINTS_URLhttps://raw.githubusercontent.com/apache/airflow/constraints-2.10.3/constraints-3.11.txt \ .为什么不建议在Dockerfile里写死RUN pip config set global.index-url ...因为写死了就失去可移植性换个内网环境还得改Dockerfile。用--build-arg在构建时传源地址同一个Dockerfile既能连公网源也能连公司源。需要说明的是constraints文件本身在GitHub raw上国内访问也可能慢可以提前下载放进构建上下文或者放到内网对象存储。4.3 离线依赖包把wheels提前备好如果构建机完全离线还有一个更彻底的办法在有网的机器上先把所有依赖下载成wheel文件再在Dockerfile里用--no-index --find-links安装。mkdir -p wheels pip download -r requirements.txt -d wheels \ -i https://pypi.tuna.tsinghua.edu.cn/simple \ -c constraints.txtDockerfile里对应改成COPY wheels/ /tmp/wheels/ RUN pip install --no-cache-dir \ -r /tmp/requirements.txt \ --no-index \ --find-links/tmp/wheels这个方案的额外好处是构建完全可重复wheel文件本身还可以作为资产存档。要注意的是下载wheel的机器Python版本、系统平台要跟目标镜像保持一致否则可能会下到很多tar.gz源码包而目标机器上缺少编译工具链导致装不上。顺带说一个越来越常见的场景如果Airflow DAG里有机器学习模型推理环节模型文件往往有几百MB甚至几个GB不建议塞进镜像更不建议在容器启动时实时从公网下载。可以考虑用内网对象存储或共享存储第一次按需加载后缓存。网络上常见的HuggingFace镜像站可以解决下载慢的问题但生产环境更稳定可靠的做法还是把模型放到离容器最近的内网存储里。另外还有像ollama这类本地模型运行环境如果Airflow的DAG要调用本地模型服务镜像里只需要装客户端模型服务外置到GPU机器。镜像里硬塞模型不仅体积失控还会导致每次模型升级都要重新构建镜像非常不划算。5. build完不等于跑通镜像验证与启动排错镜像构建成功只能说明Dockerfile没有语法错误不能说明Airflow能正常运行。最快的验证方式是直接用standalone模式把它拉起来docker run --rm -p 8080:8080 airflow-custom:2.10.3 standalonestandalone模式会初始化SQLite元数据库并同时启动webserver和scheduler很适合做镜像完整性验证。等日志里出现 “Access your Airflow instance” 提示后打开http://localhost:8080/health正常情况下会返回JSON其中metadatabase和scheduler两项都是 healthy。这一步里如果出现scheduler状态不是healthy多半是依赖或配置问题不要急着往下走。5.1 再用docker compose做贴近生产的验证单机验证通过后建议用docker compose做一次贴近生产的验证至少要把Postgres带上因为SQLite不能模拟真实并发场景。下面是一个最小可用的compose文件services: postgres: image: postgres:14 environment: POSTGRES_USER: airflow POSTGRES_PASSWORD: airflow POSTGRES_DB: airflow volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U airflow] interval: 5s retries: 10 airflow: image: airflow-custom:2.10.3 command: standalone ports: - 8080:8080 environment: AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresqlpsycopg2://airflow:airflowpostgres/airflow depends_on: postgres: condition: service_healthy volumes: pgdata:这里有个必要前提你的镜像里必须装了 psycopg2-binary否则SQLAlchemy连Postgres时会直接报 “No module named psycopg2”。我在requirements里一般会显式放上它不要赌基础镜像已经内置。验证时重点看几个指标webserver是否能打开8080端口、scheduler日志里是不是稳定出现 “Starting the scheduler” 和周期性的心跳、DAG列表能否正常解析。这三个都通过镜像才算基本健康。5.2 常见故障对照表把我在项目中遇到过的问题整理成一张表按出现频率排序现象根因处理办法启动报错 no module named psycopg2 / pymysql对应数据库驱动没装requirements里加 psycopg2-binaryScheduler状态长时间不healthy元数据库连接串不对、数据库没就绪check SQL_ALCHEMY_CONN确认Postgres健康DAG文件目录无写权限拷贝时没指定ownerCOPY加 --chownairflow:airflow系统时间差8小时未设置时区ENV TZAsia/Shanghai并同步localtimepip安装后import仍然失败包被pip装到了用户目录切root后安装或检查PIP_USER配置容器以root运行Dockerfile最后忘了USER airflow确保镜像默认USER不是root这张表看着简单但每一条背后都有人浪费过半天。尤其“包装上但import失败”这条往往是因为官方的多用户环境下pip把包装到了~/.local而airflow进程用的PYTHONPATH没有包含那个路径现象就是pip list里有、import就是找不到排查起来非常绕。6. 打标签、推内网仓库、离线搬迁镜像验证通过后就要考虑分发。标签我建议遵循“项目名: Airflow版本-构建日期-序号”的格式比如airflow-custom:2.10.3-20250115-01。光一个latest不利于回溯线上出了问题想快速回滚到上一版没标签就只能靠猜。推送到内网Harbor的流程就是标准的三个步骤打标签、推送、确认。示例docker tag airflow-custom:2.10.3-20250115-01 \ harbor.internal.example.com/airflow/airflow-custom:2.10.3-20250115-01 docker push harbor.internal.example.com/airflow/airflow-custom:2.10.3-20250115-01如果内网Harbor没有配置客户端还需要先docker login或者由CI在构建流水里统一处理。这一步经常有人忽略基础镜像apache/airflow也可以先同步到同一个Harbor以后在离线环境构建时Dockerfile里的FROM就能直连内网不用改代码。6.1 完全离线的机器怎么接收镜像有些机器不光没有外网连内网Harbor都难以访问。这种情况最简单可靠的方式是离线导出镜像docker save airflow-custom:2.10.3-20250115-01 | gzip airflow-custom.tar.gz把压缩包拷贝到目标机器后执行docker load -i airflow-custom.tar.gz需要注意docker save会把完整的镜像层都导出压缩后通常还能接受但一个带上几百MB依赖的Airflow镜像压缩包可能在1GB左右拷贝前先确认磁盘空间。如果镜像里还有大量模型文件那这个包能到好几个GB所以我前面才反复建议不要把模型装进镜像。6.2 Compose与Kubernetes里消费同一个镜像镜像进入仓库后消费方式就相对标准了。docker compose里直接把image字段换成自定义镜像名删掉build段落即可Kubernetes中如果是用airflow官方Helm chart部署在values.yaml里指定images.airflow.repository和images.airflow.tag并配置好imagePullSecrets指向内网仓库的凭据。这样调度器、webserver、worker全部使用同一个镜像版本天然一致。最后说一点个人体会。构建Airflow镜像这件事技术难度其实不高真正的成本在于“细节决策”版本锁没锁、owner对不对、源地址写没写对、离线包缓存在哪。我现在的习惯是把常用网络源、constraints文件、wheel缓存目录都作为可配置参数放进构建脚本而不是写死在Dockerfile里。这样任何一个环节出问题都不用重新发明方案调整参数重跑一次构建即可。这套流程跑了几个月最深的感受是看似最土的离线wheel目录反而是内网交付时最可靠的一环。