内网服务器要跑一个 Python 项目,联网的开发机上conda create一路绿灯,拷到机房那台机器上,conda install直接甩你一脸PackagesNotFoundError。这不是运气问题,是 Conda 环境离线部署这件事本身有几个容易被忽略的前提。我在过去几年里给不同规模的团队做过不少次这类环境搬迁,从单台机器的临时救火,到几十台机器的批量落地,踩过的坑基本能覆盖三种主流方案各自的边界。这篇就把这三种企业级无网络环境下的 Conda 环境部署方案拆开讲:环境快照搬迁(conda-pack)、内网自建本地 channel、pkgs 缓存预热加--offline,每一种我会说清楚它解决什么问题、不适合什么场景、完整命令链路长什么样、以及我在实际操作中被卡住过的具体位置。适合手里有离线机器、又不想把整个 Python 生态重造一遍的开发和运维同学,基础薄一点也没关系,命令我都给全。
1. 离线部署真正的难点不在"装",在"解析"
很多人第一次做 Conda 环境离线部署,思路很朴素:把包装上不就行了。结果一上手就发现卡住的地方跟想象完全不一样。联网环境下 Conda 干活是两步——先根据 channel 里的repodata.json把依赖关系解出来,再按解出来的清单去下载安装包。离线之后,断掉的不是第二步的下载,而是第一步的依赖解析。你没看错,绝大多数离线失败的第一现场是"找不到包",而不是"下载超时"。
1.1 联网能装不等于离线能跑:路径绑定与动态链接
Conda 环境的目录里到处埋着绝对路径。解释器的 shebang、bin/下脚本的头部、site-packages里某些包的.pth文件、编译期写入的RPATH,全都是打包那一刻所在机器的路径。你直接tar一个环境目录拷到另一台机器,解压后路径变了,python可能还起得来,但一堆脚本会报bad interpreter: No such file or directory。
这就是为什么单纯的"复制文件夹"在企业环境里经常翻车。更隐蔽的是动态链接库的问题:Python 解释器和一堆科学计算包依赖libstdc++、libgfortran、libz这些库,Conda 环境里自带了一份。如果目标机器是更老的发行版,它的系统glibc版本低于打包机上 Conda 自带的那份所要求的版本,你会看到类似version 'GLIBC_2.28' not found的报错。这个错误跟 Conda 本身没关系,是二进制兼容性的硬约束,任何方案都绕不过去,只能靠"打包机和目标机的操作系统大版本尽量接近"来规避。
1.2 repodata 才是离线后第一个断掉的东西
conda install numpy在你眼里是一条命令,在 Conda 眼里是一串动作:读取每个 channel 的repodata.json,把成千上万个包的元信息和依赖关系加载成一个索引,跑 SAT 求解器算出安装方案,然后按方案取包。离线机器上,repodata.json根本没有,Conda 连解都解不出来,报错就是PackagesNotFoundError。
理解了这一点,三种方案的分野就清楚了:
- conda-pack:跳过解析和安装,直接把"已经解算好、已经装好"的成品环境整体搬过去,目标机不需要任何包管理动作。
- 本地 channel:把联网机上的
repodata.json和对应的包文件一起搬到内网,让离线机也能重新做一次完整的解析和安装,本质是重建一个微型源站。 - pkgs 缓存预热:把解算结果和包文件都放进 Conda 的本地缓存目录,用
--offline强制它在缓存里找,属于"半成品再利用"。
1.3 三种方案的适用边界先摆清楚
选型之前先把边界画出来,比后面撞墙再回头改要省事得多。下面这张表是我自己在做方案评估时最常用的对照,你可以直接拿去和团队对齐预期。
| 维度 | conda-pack 环境快照 | 本地 channel | pkgs 缓存预热 |
|---|---|---|---|
| 目标机是否需要 Conda | 不需要(解包即可用) | 需要 | 需要 |
| 是否支持目标机重新解析依赖 | 不支持 | 支持 | 有限支持 |
| 环境体积 | 偏大,含冗余文件 | 可控,只存用到的包 | 中等,缓存会累积 |
| 多环境复用 | 差,一个环境一份包 | 好,一个源服务所有机器 | 一般 |
| 跨操作系统 | 基本不行 | 可行,分 subdir 存放 | 基本不行 |
| 增量更新难度 | 高,需重新打包 | 低,补包重新索引即可 | 中,需补下载 |
| 最适合的场景 | 单机/少量机器快速交付 | 内网多机、长期维护 | 临时救急、包量小的场景 |
看清楚这张表你会发现,三者不是替代关系,而是覆盖不同规模。一台机器,conda-pack 十分钟搞定;三十台机器要长期维护,本地 channel 才是正解;手上只有几个包、临时要跑个脚本,缓存预热最省事。下面逐个拆。
2. 方案一:conda-pack 整包搬迁,最省心的成品搬运
conda-pack 的思路非常直接:把一个已经装好的 Conda 环境打成压缩包,包里带有修正路径的脚本,目标机解压后跑一次修正脚本就能用。它的最大价值是目标机器完全不需要 Conda,也不需要网络,甚至不需要 Python 环境管理器——只要解压和运行。
2.1 打包之前必须先把环境"洗干净"
这一步十个人里有八个会跳过,然后卡在打包报错上。conda-pack 在打包时会检查环境里是否存在指向环境目录之外的文件链接,如果发现就报Files managed by conda were found to have been deleted/overwritten。最常见的原因是环境里装了以开发模式(editable install)安装的包,或者有人手动pip install了一个指向本地源码目录的包。
我习惯在打包前跑一遍体检:
# 查看环境里是否有 editable 安装 conda list -n myenv | grep -i "editable\|develop" # 查看包的安装来源,确认没有奇怪的本地路径 conda list -n myenv --show-channel-urls如果确实有 editable 包,先pip uninstall掉,把它挪到目标机上再单独装,或者干脆改成正常安装。另一个常被忽略的点是环境要尽量"干净",别把一堆临时实验的包留在里面——conda-pack 是整包复制,多一个包就多一份体积,在只能靠移动硬盘拷贝的环境里,体积就是时间。
2.2 打包与解包的完整命令链路
工具本身要先在联网机上装好。我一般不用全局装,而是建一个专用的小环境,避免污染业务环境:
# 联网机:准备打包工具 conda create -n packer python=3.10 -y conda activate packer pip install conda-pack # 打包,指定环境名和输出文件 conda pack -n myenv -o /data/myenv.tar.gz --n-threads 4--n-threads是我后来养成习惯必加的参数。默认单线程压缩,一个几个 GB 的环境能压十几分钟,开四到八个线程能省一半以上时间。压出来的包直接拷到目标机:
# 离线机:解包到指定目录 mkdir -p /opt/envs/myenv tar -xzf myenv.tar.gz -C /opt/envs/myenv # 激活并修正路径 source /opt/envs/myenv/bin/activate conda-unpackconda-unpack这一步是整包搬迁的灵魂,绝对不能漏。它做的事是把环境里所有记录的旧前缀路径替换成当前实际路径,包括脚本 shebang、.pth文件、二进制文件里的字符串常量。跑完之后可以用conda-unpack --check验证一下有没有残留,输出为空就说明干净了。
2.3 conda-unpack 只能跑一次,这个坑我踩过
conda-unpack有个特性:它执行时会修改环境内部的一批文件,包括自己的元数据。所以它只能执行一次。我第一次用的时候图省事,解压后跑了一次觉得不放心,又跑了一次,第二次直接报错,环境也没坏,但那个报错信息很容易让人误以为环境有问题,白白排查半小时。
更麻烦的场景是环境目录被移动。比如你解压到/opt/envs/myenv,跑完conda-unpack,后来觉得目录名不合适,mv成了/opt/envs/prod-env,这时候环境就废了一半——路径记录里还是老路径,而conda-unpack的有效状态已经用掉了,没法再修正。正确的做法是要么一开始就定好目录,要么重新解压一份再改目录。
2.4 跨平台和跨 glibc 的硬边界
conda-pack 的坑集中在平台兼容上,这里必须说清楚,不然容易在交付前一夜翻车。
- 不能跨操作系统:Linux 上打出来的包不能直接在 Windows 或 macOS 上用,反过来也不行。二进制格式、路径分隔符、动态库后缀全都对不上。
- 可以跨同架构的不同机器:同是 x86_64 Linux,内核版本的差异通常不影响。
- glibc 版本只能"老打新用",不能"新打老用":打包机的
glibc版本要小于等于目标机的版本。反过来会报version GLIBC_2.xx not found。所以如果知道目标机是比较老的系统,打包时就应该选一台同等或更老系统的机器来做,别在最新的开发机上一顿操作。
我个人的经验是:面对一批不确定系统版本的离线机器,先抽一台跑ldd --version看一下glibc,然后专门准备一台"老系统打包机"作为标准打包环境。这件事前期花半小时,后期能省掉一堆返工。
3. 方案二:自建本地 channel,把内网改造成小型源站
如果离线机器不止一台,或者需要长期在内网维护环境,conda-pack 就不合适了——每台机器一份包,体积浪费严重,加个包还要重新打。这时候要做的是在内网搭一个本地 channel,让所有离线机指向它,用起来跟公网源一样。
3.1 为什么不直接拷 pkgs 目录,非要走 conda index
Conda 找包靠的是repodata.json这个索引文件,里面记录了当前目录下所有包的名称、版本、构建号、依赖约束和文件校验和。你只把.conda或.tar.bz2文件拷过去,没有索引,Conda 等于看着一屋子书但没有目录卡,扫不出来。
conda index就是生成这个目录卡的工具。它扫描指定目录下的所有包文件,解析每个包的元数据,生成repodata.json和对应的.bz2压缩版。生成的索引会被 Conda 缓存,后续解析直接读它。
# 典型目录结构,subdir 分层是必须的 /data/local-channel/ ├── linux-64/ │ ├── numpy-1.24.3-py310h...conda │ ├── numpy-1.24.3-py310h...tar.bz2 │ └── repodata.json ├── noarch/ │ ├── python-dateutil-2.8.2-pyhd...conda │ └── repodata.json └── win-64/ └── ... # 生成索引,针对每个 subdir 单独跑 conda index /data/local-channel/linux-64 conda index /data/local-channel/noarchnoarch目录千万别忘。纯 Python 的包(很多工具库都是)放在noarch,如果你的环境依赖里有纯 Python 包却没索引它,解析阶段会直接失败,报错还特别隐晦。
3.2 从联网机下载依赖闭包的两种姿势
问题来了:怎么知道要下载哪些包?有两种做法,我会根据场景选。
第一种是用目标环境的显式清单去下。联网机上先建一个临时环境,装完需要的包后导出清单:
# 联网机:构建参考环境并导出精确清单 conda create -n ref python=3.9 numpy pandas scikit-learn -y conda list -n ref --explicit > /tmp/ref-explicit.txt # 按清单批量下载,--download-only 只下不装 conda create -n dl --download-only --file /tmp/ref-explicit.txt -y--explicit导出的清单是带完整 URL 和校验和的,里面每一行就是一个确切版本的包。这种方式的可靠性最高,因为版本已经锁死了,不会出现"联网机今天解析出来的版本和目标机明天解析出来的不一样"。
第二种是用名称约束让 Conda 自己解:
# 联网机:只下载不安装,包会落到 pkgs 目录 conda create -n dl2 --download-only -c conda-forge python=3.9 numpy=1.24 pandas -y这种方式的好处是省事,坏处是版本可能漂移,而且包会落在 Conda 的 pkgs 缓存目录里,需要自己去翻。我一般只在临时实验的时候用它,正式交付还是用--explicit。
下载完成后,把 pkgs 目录里的包文件按 subdir 分类拷到本地 channel 对应目录下。这里有个细节:Conda 的 pkgs 缓存里同时存在.conda和.tar.bz2两种格式的同一个包,你只需要留.conda(新格式,体积更小、解压更快),删掉.tar.bz2能省不少空间。
3.3 起一个静态文件服务,配置 .condarc
索引生成好之后,最省事的发布方式是起一个静态 HTTP 服务。不需要什么复杂的服务端,Python 自带的就够:
cd /data/local-channel nohup python -m http.server 8000 --bind 0.0.0.0 > /var/log/channel.log 2>&1 &然后在离线机的.condarc里把这个地址加进去:
channels: - http://10.0.0.5:8000 - defaults channel_priority: strict show_channel_urls: truechannel_priority: strict值得单独说一句。默认的松散优先级下,Conda 可能会为了满足某个依赖,从后面的defaults去拿包,而离线机上defaults根本不通,结果就是一边解析一边卡住。设成strict之后,它只在第一个 channel 里找,行为可预测。
不带网络服务也行,直接挂本地目录:
channels: - file:///data/local-channel适合那种内网机器少、又不想额外跑服务的场景。缺点是共享不方便,每台机器都得同步一份目录。
3.4 优先级和缓存这两个雷区
第一个雷是defaults放在最后一个 channel 位置。前面说过,离线机连不上它,只要解析走偏到那里,就会长时间挂起直到超时,看起来像"卡死"。我一般的做法是离线机上把defaults整个删掉,只留内网源。
第二个雷是 channel 缓存没刷新。conda index更新了repodata.json,但客户端本地有pkgs/cache/下的旧副本,会继续用旧索引,导致"明明刚放了包却找不到"。清理方法:
conda clean -i # 只清索引缓存,不动包 conda search numpy --override-channels -c http://10.0.0.5:8000排查时用--override-channels强制只查这一个源,能看到包就说明索引和服务都没问题,问题在客户端配置;看不到就是服务端那边索引没生成好。
4. 方案三:pkgs 缓存预热 + --offline,最轻量的应急打法
手上只有一台离线机,需要的包也不多,搭 channel 属于杀鸡用牛刀。这时候最轻的做法是把 Conda 自己的包缓存"喂饱",然后强制它离线工作。
4.1 用 --download-only 把缓存和索引一起做出来
关键在于理解--offline的机制:它禁止 Conda 访问任何网络,所有包和索引都从pkgs_dirs指向的本地目录里找。所以你不仅要放包文件,还得把repodata.json的缓存也带过去。
联网机上这么做:
# 联网机:完整下载依赖闭包,包会落在 pkgs_dirs 里 conda create -n tmp-dl --download-only python=3.9 numpy pandas matplotlib -y # 找到缓存目录 conda info | grep "pkgs directories"假设输出是/home/user/miniconda3/pkgs,这个目录里会有三样东西:解压后的包目录、.conda/.tar.bz2原始文件、以及cache/子目录里的repodata.json缓存副本。三样都要拷,缺一不可。
拷贝到离线机后,改离线机的.condarc:
pkgs_dirs: - /data/conda_pkgs offline: truepkgs_dirs放在pkgs默认位置也行,但企业环境里系统盘往往不大,我习惯单独挂一块数据盘专门放缓存,顺便方便备份。
4.2 离线机上执行安装的正确姿势
配置好了之后,安装命令加上--offline:
conda create -n myenv python=3.9 numpy pandas matplotlib --offline -y这里有个非常关键的经验:版本约束写得越死,成功率越高。--offline模式下 Conda 只能在自己的缓存里解算,如果约束写成numpy,它会去找"缓存里最新的能满足依赖的版本";但如果你的依赖链里有另一个包要求numpy<1.23,而缓存里恰好没有满足的组合,解析就会失败。写成numpy=1.24.3反而稳,因为冲突空间被压缩了。
4.3 --offline 报错的三条排查链路
--offline失败基本集中在三种报错上,我按出现频率排个序,你可以照着链路查。
第一条链路:PackagesNotFoundError: The following packages are not available from current channels
一路查下来是这样的顺序:先用conda list确认包是不是真的在缓存里;再用ls /data/conda_pkgs | grep 包名看文件名,注意版本和构建号;最后用conda search 包名 --offline看 Conda 能不能从本地索引里识别到它。如果ls看得到但conda search看不到,那就是cache/里的索引缓存没拷过来或者过期了。
第二条链路:索引存在但解析失败,报UnsatisfiableError
这种不是缺包,是版本组合无解。排查方法是先用--dry-run跑一遍看它推荐什么:
conda create -n test python=3.9 numpy pandas --offline -y --dry-run输出的求解结果会告诉你它打算装哪些版本,对照一下缓存里的版本,就能看出是哪个包的约束卡住了。实在解不出来,就把约束放宽一个层级,比如python=3.9改成python=3.9.18。
第三条链路:报CondaHTTPError或者长时间卡住
这说明offline: true没生效,Conda 还在尝试访问网络。检查.condarc的offline字段是不是写在了正确的层级,以及命令行上是不是漏了--offline。企业环境里如果HOME被设置成了别的位置,.condarc可能根本没被读到,用conda config --show-sources确认一下实际生效的配置文件路径。
4.4 换成 micromamba 会有额外收益
如果你的离线机器资源紧张,或者环境很老,micromamba是个值得考虑的替代。它是一个静态编译的单文件可执行程序,不依赖 Python 环境就能跑,体积只有几 MB,离线分发的时候特别友好。
它对离线的支持也做得更彻底,--offline之外的很多行为可以直接通过环境变量控制。用法上跟 conda 命令基本兼容,包缓存目录的机制也类似,把同一份 pkgs 目录喂给它就能直接工作。我在做一些嵌入式或者容器内的最小化场景时,会优先选它,省掉装一整个 Conda 发行版的磁盘开销。
5. 三种方案怎么选:一张决策表和几条实战判断
讲完三种方案,回到选型。这张表是我实际交付时用的决策依据,你可以直接对照自己的场景。
| 判断条件 | 推荐方案 | 理由 |
|---|---|---|
| 离线机 ≤ 3 台,一次性交付 | conda-pack | 半小时能搞定,不用搭服务 |
| 离线机 ≥ 10 台,需要长期用 | 本地 channel | 一份源站服务所有机器 |
| 目标机不允许装 Conda | conda-pack | 解压即用,零依赖 |
| 需要频繁加包、改版本 | 本地 channel | 补包重新索引,增量成本低 |
| 只是临时跑个脚本 | pkgs 预热 | 配置最少,一条命令 |
| 目标机系统较老(glibc 低) | 本地 channel | 让 Conda 在目标机上自己解算,避免二进制不兼容 |
| 有 Windows 和 Linux 混合环境 | 本地 channel | 分 subdir 管理,一套服务两端用 |
有几条判断是我吃过亏之后才认可的,写在这里供参考。
第一条:能不跨平台就不跨平台。我见过为了省事,想在一台 Linux 上打好的包直接给 Windows 机器用的方案,折腾两天最后放弃。真遇到混合环境,老老实实分 subdir 维护两个 channel。
第二条:打包机的系统版本要"向下兼容"。面对一批不确定版本的离线机,选一台系统版本最老的机器做打包机,成功率最高。这件事的收益会随着机器数量线性放大。
第三条:体积不是小事。一个完整的科学计算环境打包后常常有 1.5 GB 到 3 GB。在只能靠移动介质搬运的场景里,这个体积直接决定了你要跑几趟。用--n-threads压缩、删掉重复的.tar.bz2、清理环境里用不到的包,这些操作能省掉三成以上的体积。
第四条:把清单锁起来。无论用哪种方案,我都会导出一份environment.yml和一份--explicit清单一起归档。前者的可读性好,方便人看;后者的可复现性好,方便机器装。这份东西在半年后环境出问题、需要重建的时候,价值极高。