这份文档主要介绍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 会:
- 读取 Dockerfile
- 执行 COPY requirements.txt ,把你的依赖文件放进镜像
- 执行 RUN pip install -r requirements.txt,安装所有列出的库
- ……(其实就是把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的一个功能就是很方便地管理这些容器。