想必不少人都经历过被深度学习复杂的环境配置所支配的绝望,当你在自己的电脑上辛苦配好了环境,写好了代码,训练好了模型,搞好了服务,大功告成准备部署到另一台服务器运行时,环境又要重新配.....
个人开发时,便捷的获得配置好的环境可以节约一大笔时间;多人开发同一个项目时,统一环境配置可能会解决很多不必要的问题;在企业中开发或者测试有着标准的环境配置,开发人员使用标准环境配置正常运行,部署过程能够更加的从容流畅...
之前见一位师兄很多工作都会使用Docker进行,当时并不理解,直到后来接触到并稍微熟悉了Docker的操作,才发觉真的很好很强大。轻量级,启动迅速,可移植性高,隔离性强,功能强大的Docker,让环境配置和移植不再是令人头疼的难题。
一、认识Docker
Docker的安装本文就不详细展开了,搜索系统版本+Docker安装(例如:Ubuntu 16.04 Docker安装) 即可获得一大批的教程文章,接下来简单讲一下Docker中的几个概念:
Docker:用于支持创建和使用linux容器的容器化技术;
镜像(Image):特定环境/配置/文件的集合;
容器(Container):与系统其它部分隔离的一系列进程,从镜像运行,由镜像提供支持进程的全部文件;
仓库(Repository):镜像统一存放的场所,Docker hub 类似于github。
在使用过程中,我们可以通过拉取公开的镜像或者自己通过Dockerfile创建指定环境镜像的方式获得Docker镜像,然后运行这个镜像为一个容器,在容器中可以下载/安装应用,创建/修改文档,编写/运行程序等等,这个容器就类似于一个配置好特定环境及文件的全新的linux系统,可以在里面自由自在的操作而不影响容器之外的其他环境。
Docker,镜像,容器,仓库的关系如下图:
通过Dockerfile可以构建镜像;
镜像可以通过tag来打标签或更改名字;可以打包为压缩包,并可以从压缩包中加载;可以推送至镜像仓库,也可以从镜像仓库中拉取。
容器由镜像运行,可以停止、重启以及进入内部修改;也可以从容器重新构建镜像。这些操作在接下来都会一一展示。
二、使用Docker
1. Docker常用命令
# 查看本机的镜像及IDdocker images# 查看全部容器及IDdocker ps -a# 停止容器docker stop # 删除容器:docker rm # 如果该容器正在运行,需要先停止容器;或者添加参数-f直接停止并删除容器# 删除镜像docker rmi # 如果该镜像有容器正在使用,需要先删除容器# 重启容器docker restart
2. Docker hello world
# 拉取hello-world镜像docker pull hello-world# 运行hello-worlddocker run hello-world# 输出 ‘Hello from Docker!’ 字样,表示hello-world成功运行。
运行后输出‘Hello from Docker!’等字样,表示hello-world成功运行。
通过 docker ps -a 可以以看到当前机器上容器的信息,分别为 容器的ID ,容器从哪个镜像启动的,容器内的默认指令,容器的创建时间,容器当前的状态,端口情况,容器的名字。
这里我们运行hello-word时并未指定名字,所以docker随机给了个名字,可通过 docker run --name hello_test hello-world 为容器指定名字。
3. Docker 国内镜像源
Docker hub提供大量官方及用户上传的镜像:https://hub.docker.com/
可以为docker添加国内镜像源来提升拉取的速度。编辑 /etc/docker/daemon.json 文件(若不存在则新建),添加内容:
{"registry-mirrors": "https://docker.mirrors.ustc.edu.cn","https://alzgoonw.mirror.aliyuncs.com","https://registry.docker-cn.com" ]}
配置之后,重启docker令配置生效,查看配置。
sudo systemctl restart dockerdocker info
4. 拉取镜像
查询镜像
docker search tensorflow
拉取TensorFlow镜像
docker pull tensorflow/tensorflow:1.12.0-py3 # CPU版docker pull tensorflow/tensorflow:1.12.0-gpu-py3 # CPU版
拉取镜像后可以通过 docker images 查看本机存在的镜像及对应的镜像ID等信息。
5. 镜像标签
通过docker tag命令,可以为镜像更改标签或名字,默认标签为latest
docker tag tensorflow/tensorflow:1.12.0-py3 my_image:v1
6. 启动容器
docker run -it -p 8888:8888 --name tf_test tensorflow/tensorflow:1.12.0-py3# docker run 参数说明--name : 设定容器名字为tf_test-it : 以交互形式运行,像linux命令行一般与镜像“对话”-p : 进行端口映射(例如8008:8888为将容器内的8888端口映射到本地的8008端口,外部可通过8008端口访问)-d : 后台运行--rm : 运行结束后删除此容器 (不可与-d同时使用)--gpus:指定显卡(例如--gpus device=5,则令此容器使用5号显卡)
tensorflow镜像设定默认以运行jupyter,复制显示的链接,将ip替换为自己的ip,即可进入jupyter页面。
程序参数
若项目中默认启动程序需要指定输入参数,可以在docker run 命令的最后添加程序中的指定参数,例如--batch_size=16
- 退出当前容器
以交互式方式运行容器后。镜像内程序正在运行使用CTRL+C终止程序后,容器即退出并停止。可用 docker ps -a 查看容器id, docker rm 删除容器;若容器正在运行,想同时停止并删除容器,可在 rm 后加 -f 参数直接停止并删除容器。
(删除容器并不会影响镜像,若不删除容器则下次不可以以相同名字启动新的容器)
- 若不需要长期运行的容器,也可以在 docker run 后加参数 --rm 完成上述操作,退出容器后直接删除容器。
7. 重启停止的容器
docker stop # 停止容器docker restart # 重启容器
为了让容器能够一直运行,防止关机等操作使容器停止,也可以在docker run 后加 --restart=always 参数,让容器在停止后可以自动重启。容器重启后,与退出前的最后状态一致,例如我们在容器中创建了新文件 test.py ,停止容器并重新启动后,文件仍然在容器中。
8. Docker拷贝文件
通过 docker cp 命令,可以方便的将本地文件拷贝至容器内,或将容器内文件拷贝至本地。
docker cp :# 为本地文件路径# 为容器内路径
9. 进入正在运行的容器
docker exec -it bash# bash : 运行bash,可以使用命令行像操作本机一样操作镜像
退出时可输入 exit ,退出后容器仍在运行。
进入容器后,我们可以像在本机操作一样在容器内进行操作,例如创建/修改文件,安装/删除应用等等。例如可以通过 touch test.py 新建一个文件;可以通过 apt-get update 后 apt install vim 安装 vim ,从而修改文件内容;可以通过 pip install numpy 安装 numpy 包。
至此,我们可以像使用一台新的机器一样在容器内自由自在的操作了。
10. Docker挂载文件
如果有配置文件需要经常变动,又不想每次都进入容器修改,那么可以将本地文件夹映射到容器路径下,让容器启动后访问本地路径的文件。
docker run -v :# 为本地文件夹绝对路径# 为容器内文件夹绝对路径
11. 查看容器的log
当容器运行起来后,我们想实时查看容器的log输出,可使用 docker logs 命令。
docker logs -f -t --tail 100 # docker logs 参数说明-f: 实时跟踪容器日志-t:显示时间戳--tail 100:查看最新100行的日志
查看某段时间内的日志:
docker logs -t--since="2020-06-22T10:00:00" --until "2018-06-22T11:00:00" # docker logs 参数说明--since:从此时间戳开始的日志(也可为相对时间,如30m,则为最近30分钟的日志)--until:截止至此时间戳的日志
12. 提交镜像
如果我们修改了容器内的文件,想保留这个修改,以便将镜像部署至其他机器后仍保持此改动,可将容器创建为镜像。
docker commit
通过 docker exec 和 docker commit ,在我们想修改镜像时,不需要再重新 build ,可以直接从现有的容器修改生成新的镜像。
13. 推送镜像例如我们在名为 tf_test 的容器中,新建了 test.py 文件,此时使用 docker commit tf_test tensorflow/tensorflow:1.12.0-py3 ,则此镜像中就包含了 test.py 这个文件。若新的镜像名和标签与之前一致,则会直接覆盖掉之前的镜像。若不一致,则新的镜像依赖于之前的镜像,删除时若不删除新的镜像,则无法删除旧的镜像。
要把镜像推送至仓库时,可通过 docker login 登陆镜像仓库,然后给自己的镜像打标签后直接推送至镜像仓库。
docker tag docker push # [REPO]:镜像仓库的地址
14. 打包镜像
镜像不仅可以通过镜像仓库的 push 和 pull 来传输,也可以打包为压缩包。
在本机打包镜像
docker save -o # example:docker save -o ./get_time_image.tar get_time:latest
将压缩包复制到其他机器,加载镜像
sudo docker load # example:sudo docker load
加载后即可使用 docker images 查看新的镜像,也可以使用之前的命令运行镜像了。
三、Docker部署
1. 使用Dockerfile制作镜像
假设我们要建立一个获取当前时间的接口的简单项目,名为 get_time ,设定端口为 80 (通常将默认端口设置为80,在容器启动时再映射至指定端口)。创建镜像时需要安装一些项目所需的python库,可以先 pip install pipreqs ,然后在项目根目录下执行 pipreqs ./ 生成 requirements.txt 。
创建Dockerfile
FROM python:3.6ENV LANG C.UTF-8# Set the working directory to /get_timeWORKDIR /get_time# Copy the current directory contents into the container at /get_timeCOPY ./ /get_time# Install any needed packages specified in requirements.txtRUN pip install -i https://mirrors.aliyun.com/pypi/simple -r requirements.txt# Run api_get_time.py when the container launchesENTRYPOINT ["python", "./api_get_time.py"]
指令说明:
FROM:此镜像基于哪个基础镜像(若基础镜像在本机不存在,则会自动拉取)WORKDIR:设定当前的工作目录,相当于linux指令cdCOPY:复制文件RUN:执行的指令ENTRYPOINT:容器启动后执行的命令
- 使用 Dockerfile,通过 docker build 命令来构建一个镜像
docker build -t get_time .# docker build 参数说明-t : 指定要创建的目标镜像名. : Dockerfile 文件所在目录相对路径,也可以指定Dockerfile 的绝对路径
创建成功后,可以用 docker images 来查看我们创建的镜像。
2. 后续操作
运行验证镜像
docker run -it -p 8123:80 --name get_time_api get_time
查看容器是否正常运行,并验证其功能。
若验证正常后需要直接运行在本机,则将其挂在后台运行即可。
docker run -d -p 8123:80 --restart=always --name get_time_api get_time
修改镜像
提交镜像
四、docker-compose
如果一个项目中需要多个docker镜像,每次手动的创建、运行、管理将变得十分繁琐。通过docker-compose可以实现Docker容器集群的快速编排,便捷的管理、运行多容器的docker项目。
1. 安装
Compose是Docker官方开源的项目,主要由python编写,可以直接通过pip安装。
pip install docker-compose
2. 常用命令
docker-compose 的使用与docker的使用命令较为相似
部分命令:
docker-compose version:查看版本信息docker-compose up:根据模板文件自动构建镜像/创建服务/启动服务等docker-compose down:停止并删除容器/网络等docker-compose exec:与docker exec相同,进入到运行中的容器内docker-compose pull:拉取服务依赖的镜像docker-compose push:推送服务依赖的镜像docker-compose rm:删除停止状态的服务容器docker-compose ps:列出项目中当前的容器docker-compose logs:查看当前项目的log输出,不同服务用不同颜色标识
部分参数:
-d:在后台运行-v:挂载目录--name:为容器指定名字--rm:容器退出后自动删除
3. 模板文件
在项目根目录下创建 docker-compose.yml 文件,该文件相当于将项目所需的docker操作都写在同一个文件下,自动执行。文件内容包含version /services /networks /volumes 几部分,结构如下:
version: '2'services: service1: image: my_image ports: - "8000:80" container_name: "my_service_1" service2: image: my_image ports: - "8001:80" container_name: "my_service_2"# networks:# volumes:
文件说明:
version:目前版本为1,2,3;version2支持更多指令,version1未来将被禁用services:项目内的服务networks:配置容器连接的网络(非必须)volumes:挂载目录(非必须)# 其中services下的内容相当于docker的操作更改了写法build: Dockerfile的相对路径或绝对路径image: 服务镜像,若不存在则自动拉取container_name:指定容器名字,若不指定则随机生成ports: 端口映射(相当于docker的-p)volumes:数据卷挂载(相当于docker的-v)command: 覆盖容器启动后的默认执行命令depends_on: 服务依赖关系,定义启动顺序,先启动被依赖的服务networks:设定网络,若两个服务设定在同一个网络下,则可以互相访问容器内部的端口
4. 实例
接下来我们根据之前提到的获取当前时间的任务来进行一次 docker-compose 的实战。首先创建 compose 文件夹,目录结构如下:
docker-compose.yml 为模板文件, get_time_now 为获取时间的项目,其中包含主程序文件,Dockerfile和依赖文件,另外我们发现程序中获取的时间和当前时间相差8个小时,然后建立 get_time_now_new 文件夹内的代码中更新了代码。
接下来编辑 yml 文件为如下内容:
version: '2'services: task1: image: get_time_now:v1.0 # 从此镜像启动容器,但此时还没有这个镜像,此镜像在task2中构建,所以下方定义依赖与task2 ports: - "8001:80" # 容器内的80端口映射至宿主机的8001端口 container_name: "get_time_1" # 设定容器名称 depends_on: - task2 # 依赖于task2,启动时task2先启动 networks: - dev # 定义网络 (与task2在同一个网络下,可以与task2互相访问容器内部端口,但不能与task3互相访问容器内部端口) task2: build: ./get_time_now/ # Dockerfile所在的相对路径 image: get_time_now:v1.0 # 为构建好的镜像起个名字,若不指定,则默认名字为compose_task2 ports: - "8002:80" container_name: "get_time_2" networks: - dev # 定义网络, 可以与task1互相访问容器内部端口 - pro # 定义网络, 可以与task2互相访问容器内部端口 task3: image: get_time_now:v1.0 ports: - "8003:80" container_name: "get_time_3" depends_on: - task2 # 依赖于task2,启动时task2先启动 networks: - pro # 定义网络 (与task2在同一个网络下,可以与task2互相访问容器内部端口,但不能与task1互相访问容器内部端口) volumes: - ./get_time_now_new/:/get_time_now # 将修正好的get_time_now_new内的代码映射至容器内,该服务可以显示正确的时间networks: dev: driver: bridge pro: driver: bridge
在项目根目录下,执行 docker-compose up -d ,服务挂在后台运行。项目中在 task2 构建镜像, task1 和 task3 依赖于 task2 , task2 最先启动。 task1 和 task2 都是原生镜像,显示的时间为8个小时前, task3 中我们将修正后的代码映射至了容器内部,故 task3 可以显示正确的时间。
参考链接:
[1] https://www.runoob.com/docker/docker-tutorial.html
[2] https://www.cnblogs.com/clsn/p/8410309.html
[3] https://blog.csdn.net/yimenglin/article/details/92624549
[4] https://www.jianshu.com/p/18bb54978bc0
[5] https://blog.csdn.net/Anumbrella/article/details/80877643
[6] https://www.cnblogs.com/minseo/p/11548177.html