【Docker 系列2】Docker 全能宝典:开发中最常见问题,一步步教你解决!
介绍
什么是Docker
Docker 是一个开源的容器化平台,用于自动化应用程序的部署。它包装软件及其依赖项到一个标准化的单元中,用于软件开发。使用 Docker 的主要场景包括:
- 应用程序的打包和部署:当您需要在不同的环境中一致地部署应用程序时,Docker 是一个理想的选择。它通过容器来确保应用程序在不同的系统和平台上表现一致。
- 微服务架构:对于采用微服务架构的系统,Docker 提供了隔离、快速部署和伸缩性,非常适合于管理和部署微服务。
- CI/CD 流程:Docker 可以很好地集成到持续集成和持续部署(CI/CD)的流程中,实现自动化测试和部署。
为什么用Docker?
- Docker提供了更高级别的隔离(操作系统级别),而 Conda 提供了较低级别(Python/R 环境级别)的隔离。
- Docker 适合部署一些深度学习环境。
- 服务器CUDA版本不允许升级或降级时,使用Docker可以解决。只要宿主机的 NVIDIA 驱动与容器内部的 CUDA 版本兼容即可正常运行使用GPU的docker容器。
- 思路:在Windows上搭建基础镜像,连进容器配置环境,通过docker commit保存镜像,再通过docker save导出,迁移到服务器上。
- 有些应用需要在Docker部署,比如coffee AI。
- 网络问题,有了镜像就可以共享给其他人,不需要其他人再下载。
容器和镜像
- 镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样。
- 镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
Dockerfile
Docker 可以通过读取 Dockerfile创建一个镜像,Dockerfile 是一个文本文档,其中包含用户可以在命令行上调用和组合镜像的所有命令。
Dockerfile常见命令:
- FROM:Dockerfile必须从From开始,它为容器设置最基础的镜像
- WORKDIR:我们的工作目录,为后面的RUN等指令设置工作目录
- RUN:运行指令
- COPY:把Dockerfile所在目录的一些文件拷贝到工作空间
- EXPOSE:容器在运行时侦听的端口,它只是声明容器运行时需要监听的端口号,需要使用 docker run 命令的 -p 参数来将容器端口映射到宿主机上。比如-p 8080:80,表示将容器的 80 端口映射到宿主机的 8080 端口上。
- HEALTHCHECK:告诉 Docker 如何测试容器以检查它是否仍在工作
- ENTRYPOINT:配置容器,里面包括了可以run的py文件,也包括了run指令,可以让容器创建时去运行某些指令
写Dockerfile的注意事项,随时补充
- From的镜像可以选择更基础的镜像。
- pip 后面增加 –no-cache-dir,减少缓存。
- 在同一个RUN里不同的命令经常用 && 来连接这是一种常见的做法,主要有以下几个原因:
- 减少图层: Dockerfile 中的每个 RUN 指令都会创建一个新的图层。通过使用 && 将多个命令连接在一起,可以减少生成的图层的数量。这样做的好处是创建更小、更简洁的镜像,并且可能提高构建速度。
- 确保所有命令都成功执行: 当使用 && 时,如果一个命令失败(返回非零退出状态),整个 RUN 指令将失败并停止执行。这意味着如果 apt update 失败,紧随其后的 apt install 不会执行。这是一种保护措施,以确保不会在部分更新的或不一致的状态下构建镜像。
- 清洁和优化: 通过在一个 RUN 指令中连接多个命令,您可以在完成必要的安装和配置后立即进行清理(例如,删除临时文件或清除缓存)。这有助于保持镜像的大小尽可能小。
- apt install 后面 rm -rf /var/lib/apt/lists/*。
常用命令
查看docker所有命令
1 | docker commond --help |
镜像操作
查看docker所有镜像
1 | docker images |
下载镜像,Tag表示版本,有些镜像的版本显示latest,为最新版本
1 | docker pull 镜像名:TAG |
删除镜像
1 | docker rmi 镜像名:TAG |
使用国内源来拉取镜像,将registry.docker-cn.com填写到镜像名前。
- Docker中国区官方镜像:https://registry.docker-cn.com
- 网易:http://hub-mirror.c.163.com
- ustc:https://docker.mirrors.ustc.edu.cn
- 中国科技大学:https://docker.mirrors.ustc.edu.cn
- 阿里云容器 生成自己的加速地址:登录:cr.console.aliyun.com
windows 可以直接在设置里填入加速的地址。
1 | {"registry-mirrors":["https://reg-mirror.qiniu.com/"]} |
获取镜像的元信息,获取镜像的详细信息,包括存放地址等等
1 | docker inspect 镜像名:TAG |
从本地的压缩包里导入镜像
1 | docker load -i 压缩包名 |
或者
1 | docker load < 压缩包所在路径 |
使用Dockerfile构建镜像,.表示Dockerfile所在路径,-t表示镜像名和TAG
1 | docker build -t 镜像名:TAG . |
如果 Dockerfile 文件不在当前目录中,或者 Dockerfile 文件的名称不是 Dockerfile,则可以使用 -f 参数来指定 Dockerfile 文件的路径和名称。
容器操作
运行容器
1 | docker run --name 容器名 -i -t -p 主机端口:容器端口 -d -v 主机目录:容器目录:ro 镜像ID或镜像名:TAG |
- -i:表示以交互模式运行容器
- -t:表示分配一个伪终端
- -p:表示将主机的端口映射到容器的端口
- -d:表示容器运行后处于后台
- -v:表示挂载主机目录到容器目录,ro表示只读
- –name:表示容器名
- 镜像ID或镜像名:TAG:表示镜像名和TAG
容器列表
1 | docker ps -a -q |
- docker ps查看正在运行的容器
- -a 查看所有容器(运行中、未运行)
- -q 只查看容器的ID
- -as 查看容器占用存储空间
启动容器停止容器1
docker start 容器ID
删除容器1
docker stop 容器ID
1
docker rm -f 容器ID或容器名
- -f 表示强制删除
- -v 表示删除容器时删除挂载的卷
- -a 表示删除所有容器
- -l 表示删除容器时删除关联的链接
- -p 表示删除容器时删除关联的端口
- -s 表示删除容器时删除关联的网络
- -t 表示删除容器时删除关联的定时任务
- -u 表示删除容器时删除关联的用户
- -w 表示删除容器时删除关联的网络进入运行容器
1
docker container prune # 删除所有没有运行的容器,谨慎操作
1
docker exec -it 容器ID /bin/bash
- 进入正在运行的容器并且开启交互模式终端
- /bin/bash是固有写法,作用是因为docker后台必须运行一个进程,否则容器就会退出,在这里表示启动容器后启动
bash。 - 也可以用docker exec在运行中的容器执行命令
查看容器使用的资源
1 | docker stats 命令会每隔 1 秒钟刷新一次输出的内容直到按下 ctrl + c |
输出的主要内容:
- [CONTAINER]:以短格式显示容器的 ID。
- [NAME]:容器名称
- [CPU %]:CPU 的使用情况。
- [MEM USAGE / LIMIT]:当前使用的内存和最大可以使用的内存。
- [MEM %]:以百分比的形式显示内存使用情况。
- [NET I/O]:网络 I/O 数据。
- [BLOCK I/O]:磁盘 I/O 数据。
- [PIDS]:PID 号。
1
2docker stats
docker stats --no-stream # 只显示一次资源情况,不实时跟踪
制作Docker镜像
两种方式,一种是根据已有镜像使用docker commit构建,另一种是根据已有镜像使用Dockerfile构建。
根据已有镜像使用docker commit构建
这一方法的核心是docker commit
- docker commit可以从容器创建一个新的镜像。
- 通过对运行中的容器进行更改,然后使用 docker commit 命令,可以将容器的当前状态保存为新镜像。
- 这种方法适用于快速的实验,但不推荐用于生产环境,因为:
- docker commit不会明确记录构建过程,缺乏可重复性和透明性,会产生所谓的黑箱镜像。
- 在容器中执行命令,会有很多文件被改动或添加。安装软件包、编译构建,那会有大量的无关内容被添加进来,将会导致镜像极为臃肿。
- 简单的Python训练环境,可以用这个方法图方便,快速构建镜像。
思路:
- 先拉取已有的镜像。
- 使用镜像运行容器后,在容器内安装项目需要的Python依赖。
- 将整个容器通过docker commit指令制作成镜像。
- 导出镜像,在服务器上导入。
拉取已有镜像
官方Pytorch镜像:https://github.com/pytorch/pytorch#docker-image
也可以从docker hub里找到官方Pytorch镜像:https://hub.docker.com/r/pytorch/pytorch/tags
https://hub.docker.com/r/pytorch/pytorch/tags
同样的pytorch和cuda版本会有两种镜像,分别是devel和runtime镜像,区别如下:
devel 镜像:
- 用途:devel(开发)镜像包含了编译和运行 CUDA 应用所需的所有依赖,包括编译器和开发工具。
- 适用场景:如果您需要编译使用 CUDA 的自定义操作或者需要编译整个 PyTorch 源代码,那么 devel 镜像是必需的。
- 大小:由于包含更多的开发工具和库,devel 镜像通常比 runtime 镜像大很多。
runtime 镜像: - 用途:runtime(运行时)镜像仅包含运行基于 CUDA 的应用程序所必需的库和工具。
- 适用场景:如果您只需要运行已经编译好的 CUDA 应用程序,而不需要进行额外的编译或开发工作,runtime 镜像就足够了。
- 大小:相较于 devel 镜像,runtime 镜像更小,因为它不包含额外的编译工具和库。
我们也可以用其他人发布的可靠的镜像:https://github.com/cnstark/pytorch-docker
这里我选择的镜像是:cnstark/pytorch:2.0.1-py3.10.11-cuda11.8.0-ubuntu22.04,它包括了一个ubuntu22.04操作系统,python3.10,gpu版本的pytorch2.0.1和cuda11.8。
拉取镜像通过docker images可以看到,已经成功拉取了镜像。1
docker pull cnstark/pytorch:2.0.1-py3.10.11-cuda11.8.0-ubuntu22.04
介绍镜像的网址一般会给出使用的方法,也就是创建容器的参数,我使用了cnstark/pytorch:2.0.1-py3.10.11-cuda11.8.0-ubuntu22.04这个镜像,它的用法和pytorch给的docker容器是一样的。
即:1
2
3
4
5
6docker run -it --rm \
--gpus all \
--net host
-v /path/to/project:/path/to/project \
-v /path/to/dataset:/path/to/dataset \
cnstark/pytorch:[TAG]
运行容器
在Windows系统的Windows PowerShell中,用反引号`表示换行。如果要在命令行中使用反斜杠\来进行换行,需要在\字符后面再加上一个空格,否则反斜杠\就会被解释为一个转义字符。
1 | docker run -it --rm ` |
Ubuntu系统,使用反斜杠\进行换行,使用下面的命令:
1 | docker run -it --rm \ |
这些参数很重要!!!
- -it:前台运行容器,关了终端就关闭容器了。这个参数表示启用交互式终端,也就是让您能够与容器进行交互。-i表示交互式,-t表示分配一个伪终端。
- -dit:后台运行容器,并启用交互式终端,也就是让您能够与容器进行交互。-d表示后台运行,-i表示交互式,-t表示分配一个伪终端。
- –rm:这个参数表示容器退出后自动删除容器文件系统,这样可以防止容器文件系统占用过多的磁盘空间。
- 在Docker容器退出时,默认容器内部的文件系统仍然被保留,以方便调试并保留用户数据。
- 在Docker Desktop里暂停掉–rm的容器后,会自动把这个容器删除,所以有时候可以不加上。
- –gpus all:这个参数表示将所有的GPU都分配给容器使用。这个参数需要在安装了NVIDIA驱动和Docker的情况下才能生效。
- –net host:这个参数表示容器将使用主机网络,这意味着容器将与主机共享网络命名空间,可以访问主机上的网络设备和服务。
- -v E:/Project/Diffusion/DiffTraj-main:/app:这个参数表示将主机上的E:/Project/Diffusion/DiffTraj-main目录挂载到容器内的/app目录中,这样容器就可以访问主机上的这个目录。
- cnstark/pytorch:2.0.1-py3.10.11-cuda11.8.0-ubuntu22.04:这个参数表示要使用的Docker镜像的名称和标签。在这个例子中,使用的是cnstark/pytorch镜像的2.0.1-py3.10.11-cuda11.8.0-ubuntu22.04标签,这个镜像包含了PyTorch深度学习框架和CUDA工具包,可以在容器中运行深度学习任务。
如果之后项目里的文件比较多,建议使用挂载的方式,挂载可以让容器和宿主机共享数据。当你将宿主机上的目录或文件挂载到容器中时,容器中的该目录或文件就会和宿主机上的目录或文件保持同步,它们之间的修改都会相互影响。
执行好docker run后,可以看到,我们已经运行了容器并且启动了交互式终端。
执行好docker run后,可以看到,我们已经运行了容器并且启动了交互式终端。
如果我们进入app目录,会看到我们的项目已经被成功挂载了。
我们输入pip list和python –version,可以看到这个容器已经安装了py3.10.11-cuda11.8.0。
安装依赖
我们拉取的镜像里只包含了最纯净的pytorch:2.0.1-py3.10.11-cuda11.8.0-ubuntu22.04,根据我们要部署的应用,可能还需要有一些依赖包需要我们手动安装。
我们可以写一个或者生成一个requirement.txt用于安装我们项目需要的其他依赖,我在目录里创建一个requirement.txt后,可以看到app目录也会有这个文件,这就是挂载的含义。
1 | colored==2.2.4 |
这里我用pip补充安装了两个包colored==2.2.4和matplotlib==3.8.0。
导出镜像
先不关闭原来的docker终端,另外新建一个终端,通过docker ps可以看到我们的容器正在运行,容器的ID是a563dee1debf。
接着我们使用docker commit命令去创建一个镜像:
1 | docker commit CONTAINER ID REPOSITORY:TAG |
- CONTAINER ID表示容器ID,通过docker ps查询
- REPOSITORY:TAG表示生成的容器名和标签,可以任意命名
这里我使用如下命令:通过docker images可以看到,已经创建了这个镜像。1
docker commit a563dee1debf difftraj:2.0.1-py3.10.11-cuda11.8.0-ubuntu22.04
我们可以根据这个镜像创建一个容器进行验证直接python main.py,发现可以运行,说明在这个镜像里已经把训练模型需要的包安装好了。1
2
3
4
5docker run -it --rm `
--gpus all `
--net host `
-v E:/Project/Diffusion/DiffTraj-main:/app `
difftraj:2.0.1-py3.10.11-cuda11.8.0-ubuntu22.04
打包镜像
我们使用docker save命令保存镜像,在Ubuntu中,可以使用gzip进行压缩:
1 | docker save REPOSITORY:TAG |gzip > 镜像名 |
这里使用命令:
1 | docker save difftraj:2.0.1-py3.10.11-cuda11.8.0-ubuntu22.04 |gzip > difftraj_images.tar.gz |
如果在windows中,可以使用下面的命令,修改成自己的路径,导出可能要一两分钟。这里将镜像名设置为difftraj_image.tar,并保存到E:\Project\Diffusion\DiffTraj-main目录。
如果要导出到windows上进行导入,后缀可以用tar,如果要导出到linux上进行导入,后缀可以用tar.gz
1 | docker save difftraj:2.0.1-py3.10.11-cuda11.8.0-ubuntu22.04 -o E:\Project\Diffusion\DiffTraj-main\difftraj_image.tar |
注意导出镜像的时候,用使用镜像的名字和TAG,不要用镜像ID,否则在导入镜像的时候,镜像的名字和TAG都会为空,需要重新命名,重新命名命令如下:
1 | docker tag [镜像id] [新镜像名称]:[新镜像标签] |
导入镜像
Ubuntu使用如下命令:
1 | docker load < 压缩包所在路径 |
Windows使用如下命令:
1 | docker load -i 压缩包所在路径 |
根据已有镜像使用Dockerfile构建
这一方法的核心是创建Dockerfile
- 使用Dockerfile创建镜像是比较标准的流程,推荐使用这种方法
- Dockerfile里面的一些命令。
- Docker创建镜像时的上下文含义。
思路:
- 先创建Dockerfile,使用From命令选择基于什么镜像。
- 根据自己的需求,在Dockerfile里使用RUN命令安装各种依赖。
- 使用docker build创建镜像。
- 使用docker run创建容器。运行一下代码,测试镜像里的依赖是否满足项目的要求,如果还缺少依赖,则回到第二步,在Dockerfile里使用RUN补充安装依赖。
- 导出镜像,在服务器上导入。
创建Dockerfile
在使用Dockerfile构建镜像时,由于Docker的原理,我们一般会将 Dockerfile 置于一个空目录下,或者项目的根目录下。
这里以轨迹扩散模型的项目为例子,完整的Dockerfile如下,我们用到了 FROM WORKDIR COPY 和 RUN 这四个命令。
1 | FROM cnstark/pytorch:2.0.1-py3.10.11-cuda11.8.0-ubuntu22.04 |
From命令:Dockerfile必备的第一条指令,指定基础镜像,可以是服务类的镜像nginx、redis、mongo、mysql、httpd、php、tomcat,也可以是操作系统镜像ubuntu、debian、centos、fedora、alpine。
From命令如果选择了很大的基础镜像,那最后制作的docker镜像占的空间也会很大。
1 | FROM cnstark/pytorch:2.0.1-py3.10.11-cuda11.8.0-ubuntu22.04 |
WORKDIR命令:设置后续指令的工作目录,一般写在COPY和RUN前面。
然后通过WORKDIR命令,确定工作目录。
1 | WORKDIR /app |
COPY命令:将文件复制到镜像的指定目录中中。
接着通过COPY命令,将复制requirements.txt到镜像里的工作目录/app。
1 | COPY requirements.txt /app |
RUN命令:RUN命令可以像Shell脚本一样执行命令,后面的内容和直接在命令行中输入的命令是一样的。
最后通过RUN命令,执行pip命令,安装Python项目的依赖。
1 | RUN pip install --no-cache-dir -r requirements.txt |
我们也可以这么写,直接用RUN去执行pip命令安装我们需要的包。
1 | # 使用 cnstark/pytorch 镜像作为基础镜像 |
创建镜像
理解 docker build 的工作原理
- Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。
- 因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。
- 当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、ADD 指令等。而 docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?
- 这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
使用docker build命令,基于Dockerfile构建镜像。
1 | docker build -t test:v1.0 . |
- 最后这个点号表示Docker 镜像构建的上下文(Context),就在当前Dockerfile所在的目录。
- 也就是表示,构建这个镜像需要的所有文件,都在这个目录,比如这里的requirements.txt。
- COPY 这类指令中的源文件的路径都是相对路径,所以我们应该把构建镜像需要的文件都复制到上下文目录中。
打包镜像
最后使用docker save打包镜像即可,见3.1.5。
使用Dockerfile从0开始构建
创建Dockerfile
Difftraj镜像,指定版本的 PyTorch、Python、CUDA 以及 colored==2.2.4 和 matplotlib==3.8.0。
1 | # 使用 Ubuntu 22.04 作为基础镜像 |
创建镜像
在建立好Dockerfile文件后,使用docker build创建镜像,再使用docker run创建容器。
- docker build 会根据你写的dockerfile文件创建镜像。
- docker run 会根据你创建好的镜像运行容器。
创建镜像,-t 表示指定镜像名称为streamlit,-f表示指定目录的Dockfile,这里用的是绝对路径。
1 | docker build -f /home/streamlit/Dockerfile -t streamlit . |
创建容器
运行镜像,-p 表示宿主机占用的端口,-v 表示挂载两个目录,–name 表示指定容器名字,方便后续停止,-d表示将容器挂载到后台运行
1 | docker run -p 8501:8501 -v /home/streamlit:/app --name streamlit -d streamlit |
浏览器里输入 http://你的服务器IP地址:8501/,即可进入streamlit应用界面,部署完成。
如果之后项目里的文件比较多,建议使用挂载的方式,挂载可以让容器和宿主机共享数据。当你将宿主机上的目录或文件挂载到容器中时,容器中的该目录或文件就会和宿主机上的目录或文件保持同步,它们之间的修改都会相互影响。
比如之后要把镜像里的/app/目录挂载到到硬盘的某个目录,比如/home/streamlit目录,使用 -v 参数即可。这样就不需要在Dockerfile文件里使用COPY命令了。在其中一个目录的修改会影响到另一个目录的修改。
1 | docker run -p 8501:8501 -v /home/streamlit:/app -d streamlit |
注意事项(更新ing)
Docker build 缓存机制
在 docker build 的过程中,Docker 会缓存每一步构建的结果。也就是说,如果你重新执行 docker build 命令时,Docker 会尝试使用缓存来加速构建过程。
Docker 缓存的工作原理:每个 Dockerfile 指令(如 FROM, COPY, RUN 等)都会创建一个新的层,Docker 会缓存每个层的结果。如果 docker build 过程中某个层的指令及内容没有变化,Docker 就会跳过这一步,并使用缓存的层。
- 确保 Docker 重新复制了修改后的文件
为了强制 Docker 在构建时重新复制修改后的文件,你可以通过以下几种方法解决缓存问题:
方法一:使用 –no-cache 标志: 你可以在构建镜像时使用 –no-cache 参数来禁用 Docker 的缓存机制。这会强制 Docker 重新执行所有步骤,包括复制文件、安装依赖等。方法二:手动修改 Dockerfile 来破坏缓存: 另一种方法是通过修改 Dockerfile 的某些部分来强制 Docker 重新构建相关的层。例如,如果你修改了代码文件,并且 COPY 指令复制了这些文件,你可以通过修改 Dockerfile 中相关步骤的顺序或其他内容来“破坏”缓存。1
docker build --no-cache -t {name} .
比如,将 COPY 指令移动到 Dockerfile 的最后,或者在 COPY 之前做一个小的修改(如添加一个临时的 RUN 命令),这样 Docker 会认为该步骤有了变化,并重新执行复制文件的操作。这样,COPY 指令就不会被缓存,因为 Docker 认为文件发生了变化。1
2
3
4
5# 先执行其他操作
RUN echo "Temporary change to break cache"
# 再复制修改后的文件
COPY . /app - 重新启动容器时确保使用最新的镜像
确保在执行 docker run 启动容器时使用的是你刚刚构建的最新镜像。特别是,如果你有多个版本的镜像,可能会不小心启动旧的镜像。
可以通过以下命令确保使用最新构建的镜像:1
docker run -p 5000:5000 --rm {name}
- –rm:这个选项会在容器停止后自动删除容器,避免你重复使用旧容器。
- imagemagick:确保这指向你最新构建的镜像。
参考文档:
Docker build 网络问题1
如果你利用docker build执行Dockerfile创建镜像时,在第一行命令,例如FROM python:3.9,就遇到了网络错误。但是你检查你的网络连接并没有问题的话,你可以先执行docker pull python:3.9命令拉去基础镜像,然后执行docker build命令继续创建Dockerfile的镜像。(具体原因没搞明白。)