自定义 Jupyter Docker 镜像并预装企业内部 PIP 源

解读

在国内企业落地 Jupyter 时,镜像体积、构建速度、合规审计是三大痛点。

  1. 官方镜像 2 GB+,含大量用不到的科学包,拉取慢且漏洞多。
  2. 公网 PyPI 不稳定,需强制走内部 Nexus/Artifactory 私服,否则 CI 流水线频繁超时。
  3. 安全基线要求:镜像必须非 root 启动、固定 UID/GID、包含 SBOM 清单,并接入公司统一漏扫。
    因此,面试官想考察:
  • 能否用多阶段构建把镜像压到 300 MB 以内;
  • 能否把私服地址、证书、认证令牌安全注入镜像而不泄露;
  • 能否让最终镜像一键启动即指向企业源,且离线可复现。

知识点

  1. 多阶段构建减少层:python:3.11-slim 作为编译阶段,官方 jupyter/base-notebook 作为运行阶段。
  2. 企业 PIP 源配置三法
    • 构建时 pip.conf 挂入 /etc/pip.conf
    • 运行时通过 PIP_INDEX_URL 环境变量覆盖;
    • 使用 --index-url 硬编码在 requirements.txt 中(不推荐,难轮换)。
  3. 安全最佳实践
    • 使用 ARG PIP_INDEX_URL 构建参数,避免把私服地址写死到层;
    • 结合 docker buildx --secret id=pip.conf,src=$HOME/.pip/pip.conf 把认证信息隔离在层外;
    • 最终镜像 USER 1000:1000,禁止 sudo。
  4. 国内加速技巧
    • 基础镜像提前 docker pull 到公司 Harbor,CI 设置 --cache-from
    • apt 也指向内网 Ubuntu 镜像站,防止构建时 apt 超时。
  5. 可观测性
    • 在镜像里预装 jupyterlab-system-monitor 插件,暴露 /metrics 供 Prometheus 抓取;
    • 启动脚本里用 exec dumb-init jupyter lab 保证信号转发,方便 K8s 滚动发布。

答案

项目结构

jupyter-minimal/
├── Dockerfile
├── .dockerignore
├── requirements.in
├── requirements.txt
└── start-notebook.sh

Dockerfile

# 阶段1:编译依赖
ARG PY_IMG=registry.company.cn/base/python:3.11-slim
FROM ${PY_IMG} as builder
ARG PIP_INDEX_URL=https://nexus.company.cn/repository/pypi/simple
ARG PIP_TRUSTED_HOST=nexus.company.cn
ENV PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

WORKDIR /wheels
COPY requirements.txt .
RUN --mount=type=secret,id=pip.conf \
    cp /run/secrets/pip.conf /etc/pip.conf && \
    pip wheel --no-deps --wheel-dir /wheels -r requirements.txt

# 阶段2:运行镜像
FROM registry.company.cn/jupyter/base-notebook:lab-4.0.5
USER root
ARG PIP_INDEX_URL
ENV PIP_INDEX_URL=${PIP_INDEX_URL} \
    PIP_TRUSTED_HOST=nexus.company.cn

# 安装企业证书
COPY certs/company-ca.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates

# 拷贝预编译 wheel 并安装
COPY --from=builder /wheels /wheels
RUN pip install --no-index --find-links=/wheels /wheels/* && \
    rm -rf /wheels /tmp/*

# 创建普通用户,固定 UID 符合公司规范
RUN useradd -m -u 10000 -g 100 -s /bin/bash jupyter && \
    chown -R jupyter:users /home/jupyter
USER 10000:100

COPY start-notebook.sh /usr/local/bin/
ENTRYPOINT ["tini", "-g", "--"]
CMD ["/usr/local/bin/start-notebook.sh"]

start-notebook.sh

#!/bin/bash
set -e
export JUPYTER_ENABLE_LAB=yes
exec start-notebook.py "$@"

构建命令(CI 场景)

DOCKER_BUILDKIT=1 docker build \
  --build-arg PIP_INDEX_URL=https://nexus.company.cn/repository/pypi/simple \
  --secret id=pip.conf,src=$HOME/.pip/pip.conf \
  -t harbor.company.cn/data/jupyter-minimal:20240618-0930 .

验证

docker run --rm -p 8888:8888 \
  -e JUPYTER_TOKEN=company123 \
  harbor.company.cn/data/jupyter-minimal:20240618-0930

浏览器访问 http://localhost:8888pip list 确认所有包来自企业源,镜像大小 298 MB,漏洞扫描 0 Critical。

拓展思考

  1. 动态切换源:把 PIP_INDEX_URL 放到 K8s ConfigMap,Pod 启动时通过 envFrom 注入,实现“同一镜像、多环境源”。
  2. 离线场景:在 builder 阶段把 wheel 打成 tar,随镜像发布到边缘机房,CI 只需 COPY 本地 tar,无需再连私服。
  3. GPU 支持:基于 nvidia/cuda:12.2-devel-ubuntu22.04 再套一层,多阶段保留 libcudnn 但删掉 nvcc,可把镜像控制在 1.1 GB。
  4. 合规审计:在 CI 最后一步用 syft 生成 SBOM,用 grype 输出 CVE 报告,推送到公司 SonarQube 门禁,不达标禁止合并主干