Docker快速入门小记

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之后, 会有一些状态日志信息啥的,这个要放到后台,如果不加这个参数, 窗口就会进入阻塞的那种状态,类似jupyter
  • 6379:6379: 表示redis要暴露到外界的端口号,通过这个端口号去访问redis
  • --name: 表示指定容器的名字,redis
  • redis: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版本,项目才能运行。

所以我这里的操作有三步:

  1. 先用pipreqs命令,生成我毕设项目用到的包, 也就是原始的requirements.txt结果
  2. 在我之前可运行环境的requirements.txt中,只保留了pipreqs中的包的名字,版本号还是我之前可运行环境文件的
  3. 经过这两步筛选之后,就搞到了我项目里面的包以及版本号了, 但制作镜像过程中还有个问题,就是在装某些包的时候,会提前装一些依赖包(这也就是之前环境里面包臃肿的原因),这些依赖包依然默认装最新版本,但是我这里有几个依赖包装最新版本也不行,比如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 运行后,我们改了项目代码不会立刻生效,需要重新buildrun,很是麻烦。
  • 容器里面产生的数据,例如 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! 😉

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值