DockerImage


这份文档主要介绍docker基础镜像及其扩展,并举一个例子,仅仅作为一个简单的了解即可,更多的相关内容可以自行探索。


在不同的项目中,会有很多库都是重复的,如果每个项目都从头开始配置会导致重复下载,并且有些较大的库构建缓慢,因此就引出了docker基础镜像,它的原理是:构建一个 统一的基础镜像(Base Image),包含所有项目共用的库,然后每个项目基于它做扩展。也就是说,基础镜像的用途是封装大多数项目所共用的依赖,比如Python3.9,numpy等库,以及一些pip源配置等,它只需要构建并推送一次就可以长期复用。而针对每个项目,在基础镜像上添加项目专属依赖后就成为了项目的扩展镜像。

下面以一个简单的jupyterLab为例说明。

镜像

镜像是一个只读的模板,包含了运行程序所需要的所有东西,比如依赖库,依赖软件和代码本体等,它是不可变的。

首先,每一个镜像都至少需要两个文件,Dockerfile以及requirements。

Dockerfile

Dockerfile是一个纯文本文件,包含一系列命令,Docker 会按顺序执行这些命令,最终生成一个镜像,常用的指令格式有(简单看看就好):

指令 作用 示例
FROM 指定基础镜像(必须是第一行) FROM python:3.9-slim
RUN 在镜像中执行命令(如安装软件) RUN apt-get update && apt-get install -y gcc
COPY 将本地文件复制到镜像中 COPY requirements.txt /app/
WORKDIR 设置工作目录(后续命令在此目录下执行) WORKDIR /app
EXPOSE 声明容器运行时监听的端口(文档作用,不真正打开端口) EXPOSE 8888
CMD 容器启动时默认运行的命令(可被 docker run覆盖) CMD ["python", "app.py"]
USER 切换运行用户 USER scientist

例如一个最简单的Dockerfile

# 使用官方 Python 3.9 镜像作为基础
FROM python:3.9-slim

# 设置工作目录为/app
WORKDIR /app

# 将当前目录下的 requirements.txt 复制到镜像中
COPY requirements.txt .

# 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt

# 将当前目录所有文件复制到镜像
COPY . .

# 容器启动时运行 app.py
CMD ["python", "app.py"]

requirements

requirements.txt是 Python 项目中列出所有依赖库及其版本的文件,可以确保无论在哪个机器上安装的库版本都一致。

例如

numpy>=1.21.0
pandas>=1.3.0
matplotlib>=3.4.0
scikit-learn>=1.0.0
jupyterlab>=3.0.0

每一行是一个 Python 包,可以指定版本约束

打包

对Dockerfile文件打包成镜像可以使用(最后有个点)

-t是指明tag,也就是打包出来叫什么,-f是指明Dockerfile的路径

docker build -t my-image:1.0 -f Dockerfile .

Docker 会:

  1. 读取 Dockerfile
  2. 执行 COPY requirements.txt ,把你的依赖文件放进镜像
  3. 执行 RUN pip install -r requirements.txt,安装所有列出的库
  4. ……(其实就是把dockerfile中的步骤跑一遍,终端也会有相应提示)

jupyterLab

这个项目应该包含两个目录,一是base基础镜像,目录下有Dockerfile.base和requirements-base.txt,二是jupyter扩展镜像,目录下有Dockerfile.jupyter和requirements-jupyter.txt。

假设基础镜像中包含了一些常见的科学计算库(例如pandas,numpy),扩展镜像中增加了JupyterLab和一些可视化库

base

即base/requirements.txt内容为

numpy>=1.21.0
pandas>=1.3.0
matplotlib>=3.4.0
scipy>=1.7.0
# 所有相关项目共用的基础库

base/Dockerfile.base内容为(需要注意如果有一些指令需要root权限,则需要先切换USER为root,一般情况下更多是创建普通用户,需要的时候再进行切换,随后再切换回来,下面的Dockerfile示例图方便没有这么写)

# 使用官方 Python 3.9 镜像
FROM python:3.9-slim

# 设置工作目录
WORKDIR /workspace

# 复制依赖文件并安装
COPY requirements-base.txt .
RUN pip install --no-cache-dir -r requirements-base.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

# 删除本地依赖文件
RUN rm requirements-base.txt

# 设置默认目录
WORKDIR /workspace

使用build指令打包base镜像

docker build -f Dockerfile.base -t scientific-base:py39-v1.0 .

构建完成后可以通过docker images指令查看,如果出现scientific-base:py39-v1.0表示构建成功

接着需要推送到仓库,使用docker tag <原标签> <推送标签>给它打标签,格式是 <registry-url>/<namespace>/<image-name>:<tag>,这里以我的阿里云仓库为例,运行指令是

docker tag scientific-base:py39-v1.0 crpi-lz3ctlnv0bvd3g25.cn-hangzhou.personal.cr.aliyuncs.com/cz19997/scientific-base:py39-v1.0

然后push到仓库(可能需要先登录,这里略去)

docker push crpi-lz3ctlnv0bvd3g25.cn-hangzhou.personal.cr.aliyuncs.com/cz19997/scientific-base:py39-v1.0

推送成功后就可以开始扩展,任何dockerfile都可以用FROM <registry> /scientific-base:py39-v1.0来导入这个base镜像

jupyter

假设需要添加的库如下(即requirements-jupyter.txt):

notebook>=6.0.0
jupyterlab>=3.0.0
seaborn>=0.11.0
plotly>=5.0.0
scikit-learn>=1.0.0

这里的Dockerfile就应该FROM之前的base镜像,就是

# 使用刚刚推送的远程基础镜像
FROM crpi-lz3ctlnv0bvd3g25.cn-hangzhou.personal.cr.aliyuncs.com/cz19997/scientific-base:py39-v1.0

WORKDIR /workspace

COPY requirements-jupyter.txt .

RUN pip install --no-cache-dir -r requirements-jupyter.txt \
    -i https://pypi.tuna.tsinghua.edu.cn/simple \
    && rm requirements-jupyter.txt

# 开放端口 8888(这个是因为后面要访问jupyterlab)
EXPOSE 8888

# 启动 JupyterLab,允许所有 IP 访问,不打开浏览器
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--port=8888", "--no-browser", "--allow-root"]

进入jupyter目录后运行build指令

docker build -f Dockerfile.jupyter -t scientific-jupyter:v1.0 .

打包成功后就可以推送(方便后续再次运行这个项目,也可以不把扩展镜像推送上去),依然是先打tag再push

docker tag scientific-jupyter:v1.0 crpi-lz3ctlnv0bvd3g25.cn-hangzhou.personal.cr.aliyuncs.com/cz19997/scientific-jupyter:v1.0

docker push crpi-lz3ctlnv0bvd3g25.cn-hangzhou.personal.cr.aliyuncs.com/cz19997/scientific-jupyter:v1.0

然后就可以本地运行这个镜像(这里的一些参数都是jupyter需要的,实际使用时候参数需要自己配)

docker run -d -p 8888:8888 -v "$PWD/notebooks:/workspace/notebooks" --name jupyter-lab crpi-lz3ctlnv0bvd3g25.cn-hangzhou.personal.cr.aliyuncs.com/cz19997/scientific-jupyter:v1.0

#也可以用\分行写
docker run -d \
  -p 8888:8888 \
  -v "$PWD/notebooks":/workspace/notebooks \
  --name jupyter-lab \
  crpi-lz3ctlnv0bvd3g25.cn-hangzhou.personal.cr.aliyuncs.com/cz19997/scientific-jupyter:v1.0

没有报错就是成功运行,可以使用docker ps查看正在运行的容器,可以看到对应的image正是刚刚打包的

然后访问日志获取运行的jupyterlab的访问链接(logs 后面的是刚刚运行的容器名):

docker logs jupyter-lab

应该可以看到有两行带token的链接,第一个是运行的容器映射的8888端口,那一串是容器ID,与之前docker ps指令查看的一致,第二个是本机映射的8888端口

http://4e0326576401:8888/lab?token=ba4b6b82f02fcabdfa0c7c1478dc7dca9fb302a20bc3a258
http://127.0.0.1:8888/lab?token=ba4b6b82f02fcabdfa0c7c1478dc7dca9fb302a20bc3a258

可以直接在网址上用第二个链接访问到jupyterlab,说明我们的镜像构建对了,如果想用第一个链接,需要用docker exec指令,因为它是容器内部视角来看的链接(其实这部分没啥用,可以不管)

docker exec -it jupyter-lab bash

这个时候就进去了容器,前面变成了root@4e0326576401:/workspace# ,如果是正常情况是可以使用curl指令进行访问的,就是

curl -s http://$HOSTNAME:8888
#或者
curl -s http://127.0.0.1:8888

但是这里由于刚开始构建base镜像的时候用了python的slim版本,slim实在是太精简以至于甚至没有包含curl,wget等指令,所以到这里就不行了,需要扩展出来更多http.client的东西,扯太远了就……但是正常情况下是这样的访问的,然后你会看到一个html语言的网页界面(一堆html的标签输出,应该是)。

最后是运行完成后会自动在jupyter的目录下创建一个notebook的目录用于存放编辑的内容,没了。

一些后话

最后需要停止和删除容器,用stop和rm,删除镜像要用rmi,最后的i就是image(都可以接受id或名称为参数)

docker stop jupyter-lab
docker rm jupyter-lab

docker rmi ee7cbd482336
docker rmi allen_mysql:5.7

如果想要查看镜像中有哪些库(例如需要看看基础镜像是不是包括了某个库),可以先run起来,然后用exec指令看,具体的参数看情况

docker exec jupyter-lab pip list

如果想要执行具体的python代码,可以在Dockerfile中先COPY过来py文件(如main.py),然后用CMD指明CMD ["python", "main.py"],这样镜像在run的时候就会直接执行它;也可以调整docker run的参数(需要Dockerfile没有CMD或者允许覆盖,但是一般是允许的),例如

docker run <镜像名> python /workspace/a.py

注意需要执行的py文件一定要在Dockerfile中用COPY放在镜像内部的工作目录下。

其他

使用docker run运行启动的是Container容器,它是正在运行的镜像实例,可以把镜像和容器类比成Java中的类和对象,每个容器之间是隔离的。此外还有用于持久化数据的Volume卷,因为容器是暂时的,可以停止和删除,如果要保存数据需要用到卷,卷可以在多个容器间数据,这里不详细说了。

还有一个重要的东西是Kubernetes(k8s),它是管理Docker容器的系统,比如有一个大的分布式项目中有很多微服务,每个微服务都是运行在各自容器中的,相互之间使用REST或者gRPC什么的通信,如果手动管理的话就很困难,k8s的一个功能就是很方便地管理这些容器。


文章作者: Chen Zhou
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Chen Zhou !
  目录