- 云原生
- 开发工具
- 数据科学
【免费下载链接】docker-stacks
Ready-to-run Docker images containing Jupyter applications
本文基于仓库中的 开发环境指南 展开,系统讲解如何在本地搭建 docker-stacks 项目的完整开发环境:一次性安装 Python 依赖与 pre-commit 钩子,并在提交 PR 前用make build/<image>构建镜像、用make test/<image>运行“镜像自身 + 所有父镜像”的测试集合。读完本文,你可以独立完成一次贴近 CI 的本地检查,并理解 Makefile 与测试层级体系(IMAGE_PARENT)背后的调用逻辑。
环境与前置条件
在开始之前,本机需要满足以下四个前置条件(来自 dev-setup.md):
| 依赖 | 说明 |
|---|---|
| Docker | 构建与测试的核心引擎;pre-commit中的 Hadolint 钩子依赖 Docker 守护进程在运行 |
| Python 3.12+ | 用于运行测试驱动脚本与 tagging 工具链(代码层面也启用了pyupgrade --py312-plus) |
| GNU Make | 驱动 Makefile 中的build/%、test/%等模式化目标 |
| Git | 版本控制与 pre-commit 钩子挂载 |
一次性安装步骤
克隆仓库并安装 Python 开发依赖
# 克隆仓库后进入目录 cd docker-stacks # 安装 Python 开发依赖 pip install -r requirements-dev.txt # 安装 pre-commit 钩子(git commit 时自动运行 linter) pre-commit install --install-hooksrequirements-dev.txt 中的依赖均为精确锁定版本,其职责可以拆解为三块:
- 容器与系统交互:
docker==7.2.0(Python 版 Docker SDK,测试通过 API 管理容器)、plumbum==2.0.2(tests/run_tests.py 用它拼装并前台执行 pytest 命令); - 测试框架:
pytest==9.1.1、pytest-rerunfailures==16.7(失败自动重跑,规避容器网络抖动)、pytest-xdist==3.8.0(提供--numprocesses并行执行参数); - 工具链:
pre-commit==4.6.2、requests、tenacity(重试装饰器,用于 API 轮询等待)、tabulate、python-dateutil。
pre-commit install --install-hooks会把 .pre-commit-config.yaml 中定义的钩子注册到本地 git hooks,此后每次git commit都会对改动文件自动执行 pyupgrade、isort、black、ruff、flake8、shfmt、shellcheck、hadolint、yamllint、markdownlint、nbstripout 等检查。配置中还包含ci: autoupdate_schedule: monthly,说明钩子版本由 pre-commit.ci 每月自动更新。
PR 提交前检查清单(Pre-PR checklist)
原文档给出的三步检查是本地验证的核心流程:
# 1. 运行全部 linter(包括 mypy) pre-commit run --all-files --hook-stage manual # 2. 构建你修改的镜像 make build/<image-name> # 3. 运行该镜像的测试 make test/<image-name>其中<image-name>替换为你修改的镜像目录名,如docker-stacks-foundation、base-notebook、scipy-notebook。下面结合仓库源码逐条解析其底层行为。
第一步:为什么必须加--hook-stage manual
从 .pre-commit-config.yaml 的源码结构看,mypy和basedpyright两个静态类型钩子均显式声明了stages: [manual]:
# To work around this we run `mypy` only in manual mode # So it won't run as part of `git commit` command, # but it will still be run as part of `pre-commit` workflow and give expected results stages: [manual]原因是 pre-commit 默认只对 git 暂存区中的变更文件运行钩子,而mypy --follow-imports依赖整个项目的导入图才能得出正确结论,只检查变更文件会漏报。因此git commit时它们被跳过,只有手动执行pre-commit run --all-files --hook-stage manual时才会连同全部文件一起运行——这正是文档中第一步命令的由来。另需注意:Hadolint 钩子(hadolint-docker)通过 Docker 运行镜像执行 lint,执行第一步时Docker 守护进程必须处于运行状态。
第二步:make build/<image-name>在做什么
查看 Makefile,build/%是一个模式匹配目标:
build/%: DOCKER_BUILD_ARGS?= build/%: ROOT_IMAGE?=default_root_image build/%: PYTHON_VERSION?=3.13 build/%: ## build the latest image for a stack using the system's architecture $(CONTAINER_CLI) build $(DOCKER_BUILD_ARGS) \ --tag "$(IMG)" \ "./images/$(notdir $@)" \ --build-arg REGISTRY="$(REGISTRY)" \ --build-arg OWNER="$(OWNER)" \ --build-arg ROOT_IMAGE="$(ROOT_IMAGE)" \ --build-arg PYTHON_VERSION="$(PYTHON_VERSION)"关键参数(均可在命令行覆盖):
| 变量 | 默认值 | 含义 |
|---|---|---|
REGISTRY | quay.io | 镜像注册表前缀 |
OWNER | jupyter | 组织名,最终镜像引用为$(REGISTRY)/$(OWNER)/<image> |
ROOT_IMAGE | default_root_image | 仅对docker-stacks-foundation生效,默认使用 Dockerfile 中 sha 固定的根镜像 |
PYTHON_VERSION | 3.13 | 仅对docker-stacks-foundation生效,控制基础层的 Python 版本 |
DOCKER_BUILD_ARGS | 空 | 追加任意 build 参数的透传口 |
Makefile 还有两个值得注意的工程细节:
- BuildKit 始终启用:
export DOCKER_BUILDKIT:=1保证构建使用 BuildKit 执行器; - 容器引擎自动探测:
CONTAINER_CLI?=$(if $(shell command -v docker),docker,container)会在系统装有docker时优先使用它,否则回退到 Apple 的container框架,并相应调整image ls、image prune的参数差异(见 Makefile)。
第三步:make test/<image-name>与父镜像测试传播
test/%目标并不直接调用 pytest,而是委托给 tests/run_tests.py:
test/%: ## run tests against a stack python3 -m tests.run_tests \ --registry "$(REGISTRY)" \ --owner "$(OWNER)" \ --image "$(notdir $@)"而 tests/run_tests.py 内部通过 plumbum 执行:
python3 -m pytest --numprocesses auto -m "not info" <test_dirs> \ --registry ... --owner ... --image ...这里--numprocesses auto即上文pytest-xdist提供的并行能力;-m "not info"会跳过标记为info的信息性测试。真正的关键在于传入的<test_dirs>由 tests/hierarchy/get_test_dirs.py 递归计算:
def get_test_dirs(image: str | None) -> list[Path]: test_dirs = get_test_dirs(IMAGE_PARENT[image]) # 先递归取父镜像的测试目录 current_test_dir = IMAGE_SPECIFIC_TESTS_DIR / image assert current_test_dir.exists(), ... test_dirs.append(current_test_dir) return test_dirs而IMAGE_PARENT定义在 tests/hierarchy/images_hierarchy.py,即镜像层级关系:
IMAGE_PARENT = { "docker-stacks-foundation": None, "base-notebook": "docker-stacks-foundation", "minimal-notebook": "base-notebook", "scipy-notebook": "minimal-notebook", "r-notebook": "minimal-notebook", "julia-notebook": "minimal-notebook", "tensorflow-notebook": "scipy-notebook", "pytorch-notebook": "scipy-notebook", "datascience-notebook": "scipy-notebook", "pyspark-notebook": "scipy-notebook", "all-spark-notebook": "pyspark-notebook", }这就解释了原文档中的说明:make test/scipy-notebook会依次把docker-stacks-foundation、base-notebook、minimal-notebook、scipy-notebook四个目录(分别对应tests/by_image/<image>下的测试文件)全部针对scipy-notebook这个镜像运行一遍。也就是说,一个镜像的测试集合 = 自身测试 + 所有祖先镜像的测试,确保基础层的改动不会破坏下游行为。
与之呼应,原文档还提示了一个 CI 与本地的差异:CI 会把同一套测试集合再针对每一个下游镜像各跑一遍,因此在 CI 中修改docker-stacks-foundation会得到跨全部镜像的验证,而本地只需make test/docker-stacks-foundation。
父镜像缓存陷阱
原文档用{note}强调了本地构建的一个隐蔽问题:如果父镜像不在本地,Docker 会直接从注册表拉取它。这意味着修改了docker-stacks-foundation后,若只构建下游镜像而没重新构建父镜像,FROM拉到的仍是注册表上的旧版本,你的改动不会反映到下游。因此下文的“常见场景”示例中,改动基础层时必须按依赖顺序先构建父镜像。
常见场景速查
原文档给出的三组典型命令:
# 场景 1:修改 foundation 镜像(start 脚本、日志等) pre-commit run --all-files --hook-stage manual make build/docker-stacks-foundation make test/docker-stacks-foundation # 场景 2:修改 base-notebook(必须连同父镜像一起构建) pre-commit run --all-files --hook-stage manual make build/docker-stacks-foundation make build/base-notebook make test/base-notebook # 场景 3:构建并测试全部镜像(较慢;仅在修改 # foundation 或 base 镜像、准备开 PR 前使用) make build-all make test-all从 Makefile 可以看到build-all/test-all展开的ALL_IMAGES是按构建依赖顺序排列的:docker-stacks-foundation→base-notebook→minimal-notebook→scipy-notebook→r-notebook→julia-notebook→tensorflow-notebook→pytorch-notebook→datascience-notebook→pyspark-notebook→all-spark-notebook,这与IMAGE_PARENT映射完全一致。
辅助 Make 目标与延伸阅读
除了三步检查清单,Makefile 中还提供了若干对本地开发很有用的目标(运行make help可查看全部带##注释的目标):
| 目标 | 用途 |
|---|---|
make run-shell/<image>/run-sudo-shell/<image> | 进入容器交互式调试(后者以 root 运行) |
make check-outdated/<image> | 运行 test_outdated.py 生成过期的 mamba/conda 包报告 |
make cont-clean-all | 停止并删除所有容器,清理测试残留 |
make img-rm | 删除 dangling 镜像与 jupyter 名下镜像,回收磁盘 |
make hook/<image> | 运行 tagging 后构建钩子(写 tags/manifest 并应用标签) |
make docs | sphinx-build -W构建 HTML 文档 |
更多规范可对照仓库文档阅读:Lint 规范(Hadolint 忽略规则与# hadolint ignore=DL3001注释用法)、测试编写指南、贡献流程;钩子行为的 CI 侧对应物见 .github/workflows/pre-commit.yml 与 docker-build-test-upload.yml,前者即本文第一步--hook-stage manual命令的 CI 版本,后者即第二步、第三步的镜像构建与测试流水线。
小结
docker-stacks 的本地开发流程可以浓缩为一句话:装好依赖与钩子后,每次改动都执行「全量 lint → 按依赖序构建镜像 → 运行含父镜像在内的测试集合」三步。理解了IMAGE_PARENT驱动的测试目录递归收集、Makefile 的build/%/test/%参数化设计,以及父镜像“本地无则拉注册表”的缓存行为,你就能在开 PR 前准确预判 CI 的验证范围,避免基础层改动对下游镜像的连锁影响。
- 云原生
- 开发工具
- 数据科学
【免费下载链接】docker-stacks
Ready-to-run Docker images containing Jupyter applications
相关推荐
如何将Imposm3集成到现有GIS工作流中:完整指南 🗺️
如何将Imposm3集成到现有GIS工作流中:完整指南 🗺️ OpenStreetMap数据导入工具Imposm3是GIS工作流中的强大助手,它能高效地将OS
大数据WebDataset与自然语言处理:构建高效文本数据加载管道
WebDataset与自然语言处理:构建高效文本数据加载管道 WebDataset是一个基于Python的高性能I/O系统,专为大型(和小型)深度学习问题设计,
gsplat 开发环境搭建与工程流程:JIT 编译安装、格式化检查、本地测试与文档构建指南
gsplat 开发环境搭建与工程流程:JIT 编译安装、格式化检查、本地测试与文档构建指南 本文基于 gsplat 官方开发文档 docs/DEV.md htt
人工智能计算机视觉3D渲染图形学高性能计算
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考