1. 写在前面
最近面临毕业,导师说建议把做过的毕设项目交付给后面的学弟学妹们,并且要求能运行起来,这样就能继续在研究方向上做一些新的工作,这也算是我们实验室一贯的传统。 这里面有个要求就是要能运行起来,那么就需要两个项目的运行环境保持一致,这个有时候是很难做到的,即使我把我项目的运行环境打包起来,有时候在另一台电脑上也会出现各种各样包找不到等报错。
于是乎, 为了能让我项目在别的电脑上一键运行, 我想到了docker,这个东西之前也一直想学习的,但这种技术工具,如果没有需求,没有实践, 很快就会忘掉,今天有了需求之后,就有动力入门docker了, 这篇笔记关于docker学习, 把有关docker的常用命令以及如何把上面的python项目打包成docker镜像以方便在不同主机上一键运行过程记录下,方便以后查阅。
学习参考docker快速入门教程, 但这里我想通过我的毕设项目,把所有知识亲自实操一遍,顺便记录一些坑,希望能把知识理解的更加深入些,实践是检验真理的唯一标准 😉
大纲如下:
- Docker简介安装与常用的命令
- Docker的快速安装软件
- Docker打包python项目制作镜像
- Docker目录挂载(bind和volume)
- Docker多容器通信
- Docker-Compose
- Docker镜像发布与部署
- Docker备份与迁移数据
- 小总
Ok, let’s go!
2. Docker的简介安装与常用命令
这个不在这里整理具体过程,可以参考上面的文档, 之前我也写了一篇文章整理过大数据开发环境搭建番外之docker初识, 这里面是介绍了docker的一些基础知识,以及如何在Linux上进行docker的安装。 上面文档里面介绍了在Linux上如何安装docker以及可能遇到的报错。 后来发现了一种在Ubuntu上安装docker更简单的方式:
sudo apt install docker.io
# 安装完之后,如果是普通用户,可以运行docker命令的时候,还得加sudo docker,所以为了简单,直接把当前用户加入到docker用户组
# 这样以后就不用光sudo docker了
sudo groupadd docker # 添加docker用户组,一般已经存在
sudo gpasswd -a wuzhongqiang docker # 将用户wuzhongqiang加入到docker用户组
newgrp docker # 更新用户组docker
我这次实验是在Windows上进行的,安装的Docker Desktop。
这里整理关于docker常用到的一些命令:
"""容器相关"""
# 查看当前运行中的容器
docker ps
# 查看所有容器
docker ps -a
# 删除指定id的容器
docker rm container-id
# 停止/启动指定id的容器
docker stop/start container-id
# 重启容器
docker restart container-id
# 启动一个远程shell
docker exec -it container-id /in/bash
# 后台运行的容器查看日志,有时候docker run的时候容器会挂掉,说明容器启动过程出现问题,而如果是后台运行,我们需要这个命令来看具体为啥?
# 先docker ps -a,找到挂掉容器id,然后这个命令看下原因
docker logs container-id
# 查看docker容器的详细信息(ip地址等)
docker inspect container-name
# 主机和容器的文件拷贝
docker cp 1.txt container-id:/home/work/
docker cp container-id:/home/work/1.txt /wuzhongqiang/
"""镜像相关"""
# 查看镜像列表
docker images
# 拉取镜像
docker pull img_url
# 删除指定id的镜像,前提基于它创建的容器要不存在,先删除容器,再删除镜像
docker image rm tag_name
docker rmi image-id
docker rmi image-name
# 打镜像和发布镜像
doceker commit -m "test" container-id/name tag_name # 来自已有容器
docker build -t micr.cloud.mioffice.cn/wuzhongqiang/triton_img:v2 -f bigmodellearning/Dockerfile . # dockerfile
docker push tag_name
"""volume数据卷相关"""
# 查看volume列表
docker volume ls
# 创建volume
docker volume create volume-name
# 删除volume
docker volume rm volume-name
# 删除用不到的所有volume
docker volume prune
# 查看volume的细节
docker volume inspect
"""网络相关"""
# 查看网络列表
docker network ls
在windows上或者Linux上安装完docker, 打开命令行, 这些命令都可以直接使用。 当然在Windows上也可以使用Docker Desktop。
3. Docker的快速安装软件
这里参考的是上面文档中的demo,第一个是装redis, docker装redis的逻辑是直接去docker的镜像官网,去找redis的版本镜像,找到之后, 在命令行输入命令:
docker run -itd -p 6379:6379 --name redis redis:latest
解释下这里的参数含义:
run
: 开启docker的一个容器, 但是容器需要依赖于镜像,所以后面需要给出依赖的docker镜像,就是这里的redis:latest
, 表示镜像以及版本。-d
: 表示在后台开启这个容器,因为开启redis之后, 会有一些状态日志信息啥的,这个要放到后台,如果不加这个参数, 窗口就会进入阻塞的那种状态,类似jupyter6379:6379
: 表示redis要暴露到外界的端口号,通过这个端口号去访问redis--name:
表示指定容器的名字,redisredis:latest
: 表示镜像名字-t
: 为容器重新分配一个伪输入终端,通常与-i
同时使用-i
: 以交互模式运行容器,通常与-t
同时使用
这个命令之后, 后面的操作就是docker会先下载指定的镜像redis:latest
,然后基于这个镜像去创建容器并开启。 如果是想先下载到本地,就用下面的,两行命令:
docker pull redis:latest
docker run -d -p 6379:6379 --name redis redis:latest
所以,对于一般的软件安装,都可以通过上面的方式, 逻辑就是去docker官网找合适的镜像->然后run命令进行下载然后开启容器
. 这个和我们一般软件不一样的是,在镜像中已经配置好了容器运行所需要的所有环境,我们只需要拉镜像,然后跑就行。 而不必进行繁琐的环境配置,非常方便。 比如mysql这种的。开启了容器之后,可以通过下面命令来进入容器:
# -it 标准输入和关联伪终端,-it后跟容器ID,/bin/bash是命令,表示在该容器中运行该命令
docker exec -it 775c7c9ee1e1(容器id,通过docker ps看) /bin/bash
这样就进入容器内的命令行了,我们用redis-cli
进入redis。 后面工作之后,遇到的一个问题是,容器一般是一个linux系统,我们需要进入容器里面,然后安装一些新的工具,比如zip, 比如sudo, 这时候, 如果用上面的命令进去之后, 会发现报错说不是root用户,没有权限。 所以这里还得记录一个root用户进去的方法, 加-u root
:
# -it 标准输入和关联伪终端,-it后跟容器ID,/bin/bash是命令,表示在该容器中运行该命令
docker exec -it -u root 775c7c9ee1e1(容器id,通过docker ps看) /bin/bash
那么,如果我们需要安装的软件里面依赖于别的软件呢? 比如在WordPress里面就会用到MySQL数据库,WordPress是一个很方便建立网站的程序,如果我们想建立自己的博客,网站啥的,就可以用这样的程序, 像这样的东西里面都会用到数据库软件的。
这个处境,就是我们需要运行某个项目,而这个项目又依赖其他的软件, 第一种方法就是我们运行当前项目容器的时候, 通过一些设置指定其他软件的一些配置项, 这个拉镜像的时候一般会有说明, 第二个方式,就是用docker-compose命令的方式,把项目打包到一块,一键运行。这个方式非常好用。
对于wordpress软件,需要写一个docker-compose.yml, 把所有的软件放到一块来。
version: '3.1'
services:
wordpress:
image: wordpress
restart: always
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
volumes:
- wordpress:/var/www/html
db:
image: mysql:5.7
restart: always
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql
volumes:
wordpress:
db:
然后,进入这个文件所在目录, 开启命令行,输入命令:
docker-compose up
就可以一键建立多个容器了。
关于run命令,还需要记录一些示例:
- 运行一个在后台执行的容器,同时,还能用控制台管理:
docker run -i -t -d ubuntu:latest
- 运行一个带命令在后台不断执行的容器,不直接展示容器内部信息:
docker run -d ubuntu:latest ping www.docker.com
- 运行一个在后台不断执行的容器,同时带有命令,程序被终止后还能重启继续跑,还能用控制台管理,
docker run -d --restart=always ubuntu:latest ping www.docker.com
- 为容器指定一个名字,
docker run -d --name=ubuntu_server ubuntu:latest
- 容器暴露80端口,并指定宿主机80端口与其通信(: 之前是宿主机端口,之后是容器需暴露的端口),
docker run -d --name=ubuntu_server -p 80:80 ubuntu:latest
- 指定容器内目录与宿主机目录共享(
:
之前是宿主机文件夹,之后是容器需共享的文件夹),docker run -d --name=ubuntu_server -v /etc/www:/var/www ubuntu:latest
, 这个就是说建立完容器之后, 宿主主机的www目录和容器内的www目录的东西是共享的,有时候我们在容器内需要访问宿主主机上目录时,这个就需要指定出来。
4. Docker打包python环境生成镜像
这里就是想把自己的毕设项目打包成一个镜像的形式,打包方式开始参考的这篇文章, 这里讲的挺细致的。
看完之后,我本来觉得挺简单的,但是在真实实践的时候,就遇到了各种接连报错。还是走了一些坑的,因为上面这篇文章给出的demo太简单,只需要安装pandas就可以。 但实际的python项目环境太复杂, 用上面的方式会出现各种报错, 这里我就基于我走过的坑进行整理,然后在这个基础上一步步的修改,最终打包镜像成功。
首先, 我的python项目依赖的各种python包是基于anaconda虚拟环境创建的,依赖的python包非常多。项目的整体结构如下:
主目录是GraduationProject,首先,根据参考的文章,在这里面需要有两个文件,一个是Dockerfile,这里面给出了docker生成镜像的一些配置, 另外一个是requirements.txt,也就是要运行这个项目所需要的python环境。 这个文件我们可以进入虚拟环境,然后输入命令:
pip freeze > requirements.txt
这样就生成了requirements.txt文件,但是这里面打开一看:
这样情况,如果直接生成镜像的话, 会报错找不到这些文件目录。这种路径取决于环境,file:///URL
仅在本地文件系统上可用,你不能将生成的 requirements.txt 文件在另一台电脑上使用。 第一个坑
解决办法, 在生成requirements.txt文件的时候,换成下面的命令:
pip list --format=freeze > requirements.txt
这时候就不含@file
了。
这时候,我们编写Dockerfile文件,这个文件,我是直接参考上面的教程:
FROM python:3.7
WORKDIR ./GraduationProject
ADD . .
RUN pip install -r requirements.txt
CMD ["python", "./service.py"]
这里也解释下每一行是啥意思:
FROM python:3.7
FROM <基础镜像>
。所谓定制镜像,那一定是以一个镜像为基础。FROM 指令用来指定以哪个镜像作为基础镜像生成新的镜像。这里我们将官方Python的3.7版本镜像作为基础镜像。WORKDIR ./GraduationProject
WORKDIR <工作目录路径>
。 使用 WORKDIR 指令可以来指定镜像中的工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录。 注意,这个目录名字是创建镜像后的工作目录,不是当前看到的这个GraduationProject哈, 当然这里也可以换别的名字。理解了这一点,下面的ADD命令才好理解。ADD . .
ADD <源路径> <目标路径>
。使用ADD指令可以将构建上下文目录中的源路径目录复制到镜像内的<目标路径>
位置。
- 第一个参数
“.”
代表Dockerfile所在的目录(当前看到的这个GraduationProject),即python项目GraduationProject下所有的目录(不包括GraduationProject自身), 这里注意的一个点就是Dockerfile所在的目录是上下文, 如果想把GraduationProject本身复制, 此时会报错找不到这个目录。如果想把GraduationProject一块拷贝怎么办呢? 第一种方法, Dockerfile放到GraduationProject的同一级目录, 不过不优雅, 第二种方法, 就是构建镜像的时候, 运行docker build的命令时,要从GradruationProject同一级目录去运行, 然后自己指定dockerfile文件位置。- 第二个参数
“.”
代表镜像的工作目录GraduationProject,也就是创建镜像之后的工作目录,这个和上面不是同一个东西。- 所以该行命令会将python项目GraduationProject目录下的所有文件复制到镜像的GraduationProject目录下。这样docker镜像中就拥有了一份GraduationProject的python项目。
RUN pip install -r requirements.txt
RUN 指令是用来执行命令行命令的。这里直接安装requirements.txt 中指定的任何所需软件包。命令行命令,也就是你构建镜像的时候, 在命令行执行的相关命令。RUN是创建镜像时使用的CMD["python","./service.py"]
CMD 指令是容器启动命令,这里是在容器启动时通过python运行server.py。值得注意的是./
目录指的是当前工作目录即GraduationProject。容器启动命令,是你通过镜像创建出容器来之后,运行python项目的命令,即通过python server.py来开启python项目,与上面的RUN还是差别很大的哈。CMD是运行容器时使用的
有了Dockerfile之后,我们就可以当前目录下面开启命令行,然后输入
docker build -t 镜像名字:版本号 # 比如test:v1
# 指定docker file
# 在bigmodellearning的同一级目录运行才可以
# 如果是bigmodelearning里面使用上面命令打镜像, 会报错bigmodellearning目录找不到
docker build -t micr.cloud.mioffice.cn/wuzhongqiang/triton_img:v2 -f bigmodellearning/Dockerfile .
# 这个dockerfile里面的内容
FROM micr.cloud.mioffice.cn/wuzhongqiang/triton_img:v1
COPY bigmodellearning /home/work/bigmodellearning
这个命令运行之后,会直接找我们配置的Dockerfile文件,然后在那里面先构建基础镜像(Python3.7
),然后建立我们指定的镜像工作目录(WORKDIR
), 然后把宿主主机目录的文件全部拷贝到镜像工作目录下面(ADD . .
), 接下来执行RUN命令安装依赖,这样就创建好了python项目运行需要的环境,即镜像制作完成。
但真的有这么简单吗? No, 运行上面命令,迎来了第一个报错:
ERROR: Could not find a version that satisfies the requirement apturl==0.5.2 (from versions: none)
ERROR: No matching distribution found for apturl==0.5.2
ERROR: Could not find a version that satisfies the requirement Brlapi==0.6.6 (from versions: none)
ERROR: No matching distribution found for Brlapi==0.6.6
也就是pip需要的依赖包的时候,报有些包找不到指定的版本的, 我以为可能是版本号这个指定的不对,难道是改版本号或者直接去掉版本号,再或者直接去掉这个包?
查了查发现,没有这么简单
原来apturl、Brlapi都是linux包, 并不是python包,而pip install是安装的python包
所以,如果通过pip安装这些包,就会报找不到,即使修改版本号,或者去掉版本号都不好使,除非直接在.txt
文件中去掉这些包。 但去掉之后, 我不确定后面这个项目还能不能运行,于是就去掉,然后在Dockerfile中,用安装Linux包的方式来安装这些包。修改后的Dockerfile如下:
FROM python:3.7
WORKDIR ./graduation
ADD . .
RUN pip3 install -r requirements.txt
RUN apt update && apt install -y htop python3-dev wget \
apturl==0.5.2 \
Brlapi==0.6.6 \
CMD ["python", "./service.py"]
这样在我这边OK, 但是我这里发现, 安装的python包版本并不是我环境想要的哈,原因是我突然发现我是在root用户身份下进行的打包,并且运行的这些命令,结果打包的环境并不是我python项目所在的虚拟环境。
所以,我切换成普通用户,然后进入正确的虚拟环境,以普通身份的方式重新打包,就发现上面那些Linux包啥的都没了。
但是这时候的python包也非常多,并且有些显然是我毕设项目里面用不到的,那么有没有一种方式是只在requirements.txt里面列出我毕设用到的包呢? 这样构建镜像不就很快并且不用担心包这种错误了? 还真有, 方法如下:
# 先装一个pipreqs
# 这个工具的好处是可以通过对项目目录扫描,将项目使用的模块进行统计,生成依赖清单即requirements.txt文件
pip install pipreqs
# 然后在当前的项目目录下面
pipreqs ./ --encoding=utf8
这时候,就会在毕设目录下面自动生成一个requirements.txt文件,并且这里面的包全都是本次毕设项目用到的包。
这就非常清爽了,也就是这次只列出了我项目里面import到的那些包。当然,这里要注意下我上面图片的结果不是原pipreqs生成的requirements文件结果。 因为直接使用pipreqs命令,虽然会列出项目里面列出的包,但是版本号全都使用了最新版。但这个显然在我这里不能这么玩,就tensorflow来说,我的必须是1.x版本,项目才能运行。
所以我这里的操作有三步:
- 先用pipreqs命令,生成我毕设项目用到的包, 也就是原始的requirements.txt结果
- 在我之前可运行环境的requirements.txt中,只保留了pipreqs中的包的名字,版本号还是我之前可运行环境文件的
- 经过这两步筛选之后,就搞到了我项目里面的包以及版本号了, 但制作镜像过程中还有个问题,就是在装某些包的时候,会提前装一些依赖包(这也就是之前环境里面包臃肿的原因),这些依赖包依然默认装最新版本,但是我这里有几个依赖包装最新版本也不行,比如protobuf,Jinja2, h5py这三个,制作完镜像开启容器的时候,程序运行报错,于是乎,这三个包的版本号还需要在requirements.txt中额外指定出来。
经过这3步,就生成了我上面的requirements.txt,有了这个, 直接用原来的Dockerfile文件:
FROM python:3.7
WORKDIR ./graduation
ADD . .
RUN pip3 install -r requirements.txt
CMD ["python", "./service.py"]
然后再执行docker build -t
命令就完成了python项目的打包。
此时,我们查看下打包好的镜像 docker images
:
这里普通用户用docker我这里需要sudo, 一开始我用户名并不在sudo文件,所以报了个xxx is not in sudoers
的错误,这个解决办法是:
su # 前换到root
vim /etc/sudoers # 编辑sudoers文件
# 在里面添加上用户即可
wzq ALL(ALL) ALL
通过上面的探索,就完成了python项目的docker镜像制作。 这里比较耗费时间的是生成requirements.txt过程中遇到了一些坑。 不过最终比较优雅的解决,但是这个方法比较治标, 还想记录一个治本的方法,但是我没用。不过我觉得这个比较强大,那就是不只打包项目的python虚拟环境,而是打包项目运行的整个Linux系统, 这个无疑会使得后面修改更加方便灵活,这时候,需要修改Dockerfile。
FROM ubuntu:18.04
ENV PATH="/root/miniconda3/bin:${PATH}"
ARG PATH="/root/miniconda3/bin:${PATH}"
RUN apt update \
&& apt install -y htop python3-dev wget
RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \
&& mkdir root/.conda \
&& sh Miniconda3-latest-Linux-x86_64.sh -b \
&& rm -f Miniconda3-latest-Linux-x86_64.sh
RUN conda create -y -n ml python=3.7
COPY . src/
RUN /bin/bash -c "cd src \
&& source activate ml \
&& pip install -r requirements.txt"
这个虽然没用,但是这个更是强大, 基础环境从python3.7换成了整个Ubuntu系统,然后在这个系统里面安装了miniconda, 在miniconda里面新建了虚拟环境,然后进入虚拟环境, 安装项目的各种依赖包。 这个就是完全仿真了我毕设项目的虚拟环境了。 所以我本打算上面的方式不行再试试终极的这个,但是我那个方式竟然work了,那么我就先不试啦,不过这个Dockerfile的编写感觉可以留存下。
那么有了docker镜像之后,我这个项目应该怎么用呢? 只需要
sudo docker run -d -p 5000:5000 --name sstpredict sstprediction:v1
我项目就跑起来了:
本以为打工大功告成,结果,在宿主主机上,输入网址localhost:5000
, 显示
结果无法访问, 唉, 陷入了僵局,因为这个不是docker的原因了,肯定是端口映射的问题了。 上面的程序已经能正常运行到docker里面的127.0.0.1:5000
上,但在外面宿主主机里面通过localhost:5000
访问不到,说明这期间的映射有问题。 于是乎,接下来排查端口映射问题,参考Docker容器端口映射无法访问的问题排查。
第一步,看是否防火墙把5000端口禁了:
# 防火墙是否开启
firewall-cmd --state
# 如果开启,看防火墙对外开放了哪些端口和服务
firewall-cmd --list-ports
firewall-cmd --list-services
# 如果没有5000端口,有下面两种方式解决,第一个是直接把防火墙关掉
systemctl stop firewalld.service
# 第二个是,在防火墙上添加5000端口
# 如果只是临时打开端口,去掉第一行命令中的“--permanent”参数,那么当再次重启FirewallD服务时,本策略将失效
firewall-cmd --add-port=5000/tcp --permanent
firewall-cmd --reload
当然,我这里用这个查了一下,发现我防火墙本身是关闭的,所以这个策略没有用。
好吧,我这里的问题最终确定其实是127.0.0.1
的问题,这个127.0.0.1
只表示本机本容器中的一个虚拟网卡,只接受本容器中的应用相互通讯。所以在打包成镜像之后在外界访问不了是正常的。
这个的解决办法,就是在部署服务的时候修改地址,即在server.py文件中,把这里的host改成0.0.0.0
然后重新生成镜像,命名为了V2。此时,再重新开启一个容器,
sudo docker run -d -p 127.0.0.1:5000:6060 --name sstpredict sstprediction:v2
这里我直接启动创建时,绑定了外部的ip和端口,参考的这篇文章docker容器内部端口映射到外部宿主机端口
就会发现这里的ip地址变了:
可以和上面的那张图对比下。这样在宿主主机上,打开浏览器,输入127.0.0.1:5000
, 见证奇迹的时刻。
进入容器的命令行,就能查看毕设项目的源代码。
至此, python项目打包成docker镜像的探索完成。 当然,也可以把这个生成好的镜像传到DockerHub上或者是阿里云服务器上进行托管,这样别人如果想运行这个项目,也是直接pull这个镜像,然后一键run运行啦。 这个由于我项目有些大,并且现在已然完成了留在实验室中的需求, 就不上传到公共仓库啦,具体怎么弄可以参考这篇文章。
5. Docker目录挂载
上面,我已经把自己的毕设项目,通过docker打包成了镜像,并且docker的虚拟化容器技术,能在任何机器上一键运行我的毕设项目。
那么,这里有个问题,就是如果我的项目需要改一些代码怎么办? 就比如上面我修改server.py里面的主机ip那样,假设我想把主机ip从127.0.0.1修改成0.0.0.0。
我之前的操作就需要先把已经创建好的容器删除掉, 再把创建的镜像删除掉。 然后在我宿主主机里面的项目代码里面修改server.py,再重新docker build, 然后再重新docker run。
另外,一旦容器删除后,之前所做的修改,新添加的数据也会全部丢失,就类似删除虚拟机一样。
这一顿操作下来,10分钟就没了,因为docker build的这个过程要下载各种包,非常慢。 并且这还是一个小小的改动, 如果下次我又有改动怎么办? 比如想把代码里面的端口从6060改成5000, 这时候又得再来一次上面的这个操作,又10分钟。
如果你说,不用呀,我不是已经能从容器里面拿到项目的源代码了吗? 我直接进入容器,然后在这里修改server.py不就行,like this:
这样是没法修改的,我们打包的只是项目及需要的python环境, 是没有什么vim啥的这种东西的。
那有没有更优雅的方式解决上面的这种问题呢?
这其实就是目录挂载存在的意义, 解决的问题如下:
- 使用 Docker 运行后,我们改了项目代码不会立刻生效,需要重新
build
和run
,很是麻烦。 - 容器里面产生的数据,例如 log 文件,数据库备份文件,容器删除后就丢失了。
所以如果我们想在宿主主机内修改项目代码,能在容器中的项目代码立即同步,或者是容器里面产生的数据,能立即同步到宿主主机里面的话,就用到目录挂载这个技术。 目录挂载有3种方式:
bind mount
直接把宿主机目录映射到容器内,适合挂代码目录和配置文件。可挂到多个容器上volume
由容器创建和管理,创建在宿主机,所以删除容器不会丢失,官方推荐,更高效,Linux 文件系统,适合存储数据库数据。可挂到多个容器上。这个可以理解成本地主机和不同容器中共享的文件夹。tmpfs mount
适合存储临时文件,存宿主机内存中。不可多容器共享。
这三个可以用下面这个示意图来理解:
第三种方式几乎不怎么用,但是如果单纯的这么说,还是不知道应该怎么玩。那么就基于我上面的毕设项目来演示一遍。
5.1 bind mount 挂载方式
这个方式,就是在创建容器的时候, 通过一个-v参数,来指定宿主主机目录与容器目录的一个映射,这样就相当把主机内的目录映射到容器中。但注意-v后面的主机目录路径,必须是绝对路径才可以。
可以进行演示下,由于我当前运行的容器没有挂载,所以我需要停止掉,然后删除这个,重新用命令创建一个容器:
# 停掉当前容器
sudo docker stop 26f69b672041
# 删除当前容器
sudo docker rm 26f69b672041
删除了之后,我通过下面命令重新建立一个容器,但是创建之前,我这里先这样,从我毕设项目退出来,然后新建一个SSTAPP目录,后面将这个目录与容器进行挂载,因为挂载之后,容器内的代码会替换掉我主机上的代码,由于是实验,我也不确定会发生啥,别再弄的我主机上代码运行出问题。
接下来,我尝试下面命令重新建容器,这里有了-v参数。
sudo docker run -d -p 127.0.0.1:5000:6060 -v /home/wzq/workspace/SpatioTemporalProject/SSTAPP:/graduation -name sstpredict sstprediction:v2
结果这样搞容器竟然启动不起来了, 然后我先看了下日志,显示:
没有挂载的时候,没有任何问题,指定挂载目录之后,怎么出现了这样的一个报错? 然后我以为是上面构建镜像时候Dockfile的问题, 于是修改Dockerfile如下:
FROM python:3.7
ADD . /graduation
RUN pip install -r /graduation/requirements.txt
CMD ["python", "/graduation/service.py"]
相当于把Docker里面的工作目录改成根目录,然后在根目录里面新建了graduation目录,然后把所有文件拷贝到这里面,然后执行service.py。 这样构建完新镜像,同样会出现找不到service.py的问题。
于是乎,我明白了这个东西应该是去找了宿主目录下面是否有对应文件, 我由于是新建立了一个SSTAPP目录,这里面啥都没有,所以会找不到service.py。 所以我又尝试回到了毕设目录里面再执行上面的命令,结果跑起来了:
所以,主目录下得存在相应的文件才行。
这时候,我尝试修改毕设项目里面的service.py。
这时候, 打开容器的命令行,去查看service.py
就发现,已经和宿主主机里面的同步了。 但是程序如果想生效,需要重启容器。
sudo docker restart b201b828c1fd
这样就能够通过修改主机里面的代码,来相应操作容器内的代码了。当然,如果容器内的代码或者目录有任何改动,也会立即同步到宿主主机上去。
这里做的一个尝试就是,在宿主主机上,把我毕设项目里面保存好的模型删除掉,然后重启容器,在容器内运行代码之后,我这里的逻辑是,如果有保存好的模型,直接调用预测,如果没有,则训练让,然后预测,并保存模型。 结果发现,容器内的程序重新训练模型。 最终,主机内也有了新保存的模型。 还是非常powerful的。
写到这里,我突然又好奇,如果当时制作Dockfile的时候,不写CMD命令,而是等容器运行起来之后,从容器命令行中手动python service.py
会出现什么情景呢? 这样不更加灵活了?
结果就把Dockerfile最后一行删除掉。重新制作镜像sst:sst3
。
基于该镜像重新开启容器,就出现了容器开启,然后立马退出的情况。 即docker run后容器出现Exited(0)。
了解到原因是Docker的机制是让容器后台运行,必须至少有一个前台进程,容器运行的命令如果不是那些一直挂起的命令,会自动退出
那么如何解决这个问题呢? 之前docker run -it
参数就用上了
如果不加it参数,容器运行之后立马退出,根本不给进命令行窗口,手动运行的机会, 而有了这个参数之后,就可以进入命令行,手动python运行程序了。 这里体会到了-it参数的作用。
好奇心满足,然后把Dockfile修改回原来的, 把sst:v3容器停掉,删除, 镜像删除。 继续往下走。
5.2 Volume的方式
数据卷这个可以理解成主机与容器之间的一个共享文件夹, 一般存储数据库相关的数据。
这里我依然是用毕设项目来实验下。 首先,创建一个volume:
sudo docker volume create sst_volume
查看下详细信息:
接下来,把之前的容器删除掉,重新创建容器,并通过-v参数指定volume的名称,在bind mount中是绝对路径,而这里是volume的名称。
sudo docker run -itd -p 127.0.0.1:5000:6060 -v sst_volume:/graduation sstprediction:v2
容器开启之后,我们可以进入volume所在的地方去看下,需要root用户:
就发现在这里面有了一份和容器里面graduation中一样的目录。 在这里修改文件,同样是直接同步到容器中。
这个方式,就能和我主机里面的毕设项目耦合开了, 后面实验,我就直接通过这种方式对容器内的项目进行相应的修改了。
6. Docker 多容器通信
多容器通信说的就是,我们的web项目往往不是独立运行的,还是拿我上面的毕设项目来说,虽然我目前这个是独立的,这是因为我这个项目为了简单,里面并没有用到读写数据库的操作,全都是文件的形式进行保存。
如果我们的项目比较复杂,或者想将数据存储到数据库mysql或者redis缓存里面,这时候就需要多个容器进行协作了,说到协作,那就肯定涉及到通信,也就是如何将web容器中的数据存到redis容器,又或是如何再从redis容器中读数据到web容器?
要想多容器之间互通,从 Web 容器访问 Redis 容器,只需要把他们放到同个网络中就可以,当然,这句话很抽象,我们还是从实践中看看到底怎么玩。
这里的流程是这样,首先是在容器中项目代码里面修改service.py, 等用户注册完了之后,把用户信息从原来的保存到users.pkl文件,修改成把这个信息保存到redis容器中进行缓存。 然后新注册一个用户,看看在redis容器中能不能查到这个用户信息。
这里就需要web容器和redis两个容器了。
首先,先进入我当前项目容器中, 通过pip按照redis库,后面需要在python中连接redis。
接下来,新建立一个网络名称sst_net:
docker network create sst_net
然后开启redis容器,容器开启的时候,让它运行在sst_net网络中。
sudo docker run -itd -p 6379:6379 --network sst_net --name redis redis:latest
这时候redis容器就开启了,然后我们通过docker inspect redis-id去看看它的ip地址。这个ip地址,后面毕设项目里面要使用。 我这里redis容器的ip是172.18.0.3
。
接下来, 修改service.py代码, 由于我容器里面没有vim,无法直接从这里面修改,就在volume中修改即可。
在容器中的代码也完成了相应的修改。 就简单的在用户注册的时候, 接收到用户名密码之后,再传到redis中一份。
接下来,需要为当前的web容器指定网络,这里我直接在基础上新加入sst_net了,因为我这个容器已经关联了volume,也已经修改了代码, 没法重新删除创建。
sudo docker network connect sst_net 0a646dd923c2
这样, web容器和redis容器就在同一个网络下了。 此时重启web容器,让上面修改的代码生效。
网页进入web的注册页面,新注册一个用户,提交。此时来到redis容器下,就能够看到注册的相关信息了。
这样就完成了两个容器的通信, 如果再给redis容器创建一个volume, 这时候,web容器中的数据,就能永久保存在宿主主机里面了。
现在回顾下,多容器通信的关键是把它们放到一个网络下这句话,应该就能理解了,因为只有在同一个网络下,才能找的到。 但是这里还有个问题就是, 仅仅两个容器进行通信,上面就得这么一顿操作, 那我要是,再加几个容器通信呢? 那么还得对于每个容器,都得重新指定这个网络,然后一个个的开启来?
还是那句话,这样的方式虽然能达到目的,但不够优雅? 所以这里整理的目的是先了解下容器通信原理,实际中使用的时候,其实更多的是下面的docker-compose。
7. Docker-Compose
在上面已经运行了两个容器: 毕设项目+ Redis容器, 如果项目依赖更多的第三方软件,我们需要管理的容器就更加多,每个都要单独配置运行,指定网络实在是太麻烦,所以我们就可以使用 docker-compose 把项目的多个服务集合到一起,一键运行。官方文档
这个其实也是写一个配置文件,然后使用docker-compose相关命令进行操作。
这里我感觉,这个东西就是在打包镜像的时候,把依赖到的其他软件的配置一块写好,用docker-compose命令一起打包成一个镜像,然后一块进行运行,这样所有容器就在同一网络了。
下面尝试搞一下。 在我主机里面毕设项目目录,新建一个docker-compose.yml
文件。在这里面写下相关的配置:
version: "3" # 注意这里不能写字符串,必须是能转成int,否则后面报TypeError: ‘<’ not supported between instances of ‘str’ and ‘int’
# 下面开始写服务
services:
# 第一个服务名字,也就是我的毕设项目,后面会建立一个容器,这个是通过当前目录的Dockfile进行build, 端口映射,使用的卷名称, environment这里作用是容器默认时间改成了北京时间
app:
build: ./
ports:
- 5000:6060
volumes: # 这里如果是bind mount的方式,必须写绝对路径,否则会报错
- sst_volume:./graduation
environment:
- TZ=Asia/Shanghai
# redis容器,依赖镜像
redis:
image: redis:latest
environment:
- TZ=Asia/Shanghai
volumes:
sst_volume:
这样, 使用sudo docker-compose up
命令,就能一键打包到一块进行运行了。
这里如果我不加sudo,就会报错
ERROR: Couldn’t connect to Docker daemon at http+docker://localhost - is it running?
关于docker-compose的其他命令:
在后台运行只需要加一个 -d 参数docker-compose up -d
查看运行状态:docker-compose ps
停止运行:docker-compose stop
重启:docker-compose restart
重启单个服务:docker-compose restart service-name
进入容器命令行:docker-compose exec service-name sh
查看容器运行log:docker-compose logs [service-name]
# 删除所有容器
docker-compose rm
8. Docker镜像发布与部署
发布和部署这里,由于我毕设项目没打算发布共享,所以目前没有需求,可以先看上面的文档。 如果有需求,我再补充这一块。
后面在公司里用到了这方面的内容, 这里的需求就是自己写的代码要在自己的容器中运行, 所以这里需要把自己代码的运行环境打包成一个镜像,等到运行的时候,就去docker pull,构建容器。 这时候打包成镜像之后,是要传到云端去的。就涉及到了Docker镜像发布的内容。 这个其实涉及到之后,就很快学会。
大致流程是这样, 首先,需要先去远程云,比如阿里云这种,docker hub,去注册账号,然后建立一个属于自己的docker镜像仓库(wuzhongqiang),这时候,本地建立的镜像就可以通过命令push到这个远程仓库里面, 这个和github就很像了。
所以,具体流程如下,比如现在我们已经有一个在跑的容器, 我们想把这个容器打包成镜像发布到docker hub:
# 首先先查看正在跑的容器的id
docker ps | grep ubuntu
#CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
#651a8541a47d docker.io/ubuntu "/bin/bash" 37 seconds ago Up 36 seconds myubuntu
# docker commit :从容器创建一个新的镜像。
# docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
# -a :提交的镜像作者;
# -c :使用Dockerfile指令来创建镜像;
# -m :提交时的说明文字;
# -p :在commit时,将容器暂停。
docker commit -a "wuzhongqiang" -m "this is test" 651a8541a47d myubuntu:v1
# 这时候就把容器提交到了本地仓库, 可以用docker images | ps myubuntu查看
# 接下来推送到远程 docker push [OPTIONS] NAME[:TAG]
# 首先是登录docker hub (用户名:wuzhongqiang 密码:*******)
[root@docker-test1 ~]# docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username (wangshibo): wuzhongqiang
Password:
Login Succeeded
# 登陆成功后
docker push wuzhongqiang/myubuntu:v1
# 这样在别的机器上,就可以
docker pull wuzhongqiang/myubuntu
# 然后基于本地镜像开容器,跑自己的程序了
# 基于docker file构建镜像
# docker file自己指定
docker build -t micr.cloud.mioffice.cn/wuzhongqiang/triton_img:v2 -f bigmodellearning/Dockerfile .
docker push micr.cloud.mioffice.cn/wuzhongqiang/triton_img:v2
这就是把本地容器打包成镜像并且发布到docker hub上的一个流程, 和github超级像。
9. Docker备份和迁移数据
Docker备份和迁移数据这一块,上面已经整理了如何把容器目录挂载,挂载之后的数据永久保存到了宿主主机中,不用担心容器是否被删除。 而这一块的话,相当于,如果我已经保存好了数据,我怎么再恢复回去呢?
- 对于
bind mount
的方式,这个非常简单, 我们新建立一个容器之后,使用bind mount
挂载带了主机里面的某个目录, 这时候只需要把数据拷贝到这个目录下, 在容器内也就相应的有了数据,实现了迁移。 - 而对于volume的方式,由于数据是由容器创建和管理的,需要用特殊的方式把数据弄出来。这里主要是整理下这个,看看怎么玩。
文档中举了一个mongodb数据库的例子,而我这里没法实现这个,就依然想从我毕设项目中找需求。
我这里打算这样, 我毕设项目里面有个Dataset,是专门存放模型训练用的数据的,我打算,创建容器的时候,只把这个Dataset备份到Volume中,而不是备份所有项目了。
备份完毕之后, 我把主机中毕设项目的Dataset目录移动走,然后重新建立镜像,创建容器,此时新建立的项目是没有Dataset目录的,我通过之前备份好的Dataset恢复出来,让新建立的容器正常运行。这样就完成了数据迁移工作了。
设想是这样,我也不知道可不可行, 先干再说。
准备工作: 把当前的容器删除掉, 把之前创建的dockvolume sst_volume删除掉
新建立一个新的volume叫sst_dataset,然后基于sstprediction:v2镜像重新创建容器,此时把Dataset目录挂载到这上面来。
sudo docker volume create sst_dataset
sudo docker run -itd -p 127.0.0.1:5000:6060 -v sst_dataset:/graduation/Dataset sstprediction:v2
去volume目录看下:
这里已经存好了数据集。
接下来,新建立一个Ubuntu容器,这个容器要和宿主主机目录有Bind mount挂载,这样,我们如果把上面保存好的volume(sst_data)挂载到Ubuntu容器的上,就实现了sst_data volume与宿主主机的间接映射关系, 就相当于把毕设项目里面的Dataset间接挂载到了宿主主机中。
好吧,这么说可能听不懂,我直接操作, 首先在毕设项目目录往上退一层,新建一个SSTAPP目录,进入,在里面新建一个Dataset目录:
接下来,就新建一个Ubuntu容器,使用bind mount的方式,把容器内的Dataset与我SSTAPP下面的Dataset建立映射。 然后将由毕设项目创建好的sst_dataset volume进行压缩,放入到容器内的Dataset。命令如下:
sudo docker run [--rm] --volumes-from f48b2de08762 -v $(pwd)/Dataset:/Dataset ubuntu tar cvf /Dataset/Dataset.tar /graduation/Dataset
这个代码里面的--rm
可选,这个如果选择了会把之前创建的sst_dataset给删除掉。--volumes-from f48b2de08762
表示基于我毕设项目容器创建好的所有volumes, 后面-v参数这里是bind mount映射,最后面的tar这一块,是将我毕设容器项目下的Dataset目录进行压缩,然后放到基于Ubuntu新创建容器的Dataset目录下面,而这个目录又和主机里面的SSTAPP下的Dataset映射了起来,于是乎,这个代码执行完之后,我宿主主机里面产生了一个备份数据。
相当于毕设容器项目里面的Dataset借助另外一个Ubuntu容器,挂载到了宿主主机里面去。
接下来,把我原主机里面的Dataset移动走,然后重新打包镜像。
此时,我这个镜像里面是没有Dataset的。
接下来, 进入SSTAPP目录,把Dataset进行解压,得到新的Dataset。 接下来,用sst:v3创建容器,并把宿主主机的Dataset目录映射到新容器的Dataset目录
此时新建立的容器就能获取到数据了。
这么一大顿操作下来,其实就是间接借助了ubuntu容器实现了毕设项目容器与宿主主机的映射。
OK,这就是容器数据的备份和迁移。
10. 小总
这篇文章到这里就先结束了,大约花费了2天的时间摸索了下docker相关的知识,并通过自己的毕设项目做实验,把里面的一些坑走了一遍,整理出了docker中常用的基本命令。
docker是一个非常实用的虚拟化工具, 以后在工作中也经常会用到,所以还是有必要花时间学习一下的。 不过这次依然是借助需求的带动,从实践出发。 我之前也尝试过学习这个技术,和git一样,我发现这种东西如果单独学,不进行实践,根本就不理解,放弃了两三次,直到这次真正有了需求再上手学习,感觉就非常高效且理解运作方式了。
我隐约觉得背后又有了一种方法论:
- 有些知识,是需要从基础就开始搭建好, 比如刚入门某个新领域,这种情况我们自学的时候需要先把相关领域的所有知识快速过一遍,脑海中搭框架,做到知其然,然后再通过实践,一点点的把架子进行丰富,做到知其所以然。 这种知识,是解决实际问题的原理层次,不懂,就肯定解决不了问题,所以对于这种必须要学扎实,学深刻。
- 而有些知识,可能仅仅是工具,这种就不用花费太多时间去钻研背后的原理,比如一些即学即用的工具类(docker,git)这种,这种情况通过需求,然后直接实践,会用起来之后再去补理论,建立框架。因为这种知识是作为辅助去帮助我们解决实际问题的。以用带学,会更加有效。
如果能在学习知识之前,先把知识属性给区分开,再制定相应的计划学习,应该会帮助我们节省一些时间,干更有意义的事情。
好吧,有点偏题了, 再说回docker, 如果后面有需求再补理论, 我也会继续基于这篇文章进行整理哒。
继续Rush! 😉