今天项目需要将python项目封装成docker提供服务,参考同事的Docker封装代码一边巩固学习一边实战。首先熟悉一下docker的一些概念。然后用一个实例介绍Dockerfile的指令,然后写一个dockerfile实例,最后使用把这个镜像制作出来,并运行。
关于在镜像封装中遇到的问题,或者觉得这个笔记没有记录清楚的内容,大家也可以留言告诉我,我会定期查看留言,并更新对应的问题。
一、基础概念
1. 镜像和容器
镜像和容器就像java语言的类和类的实例
2. 镜像的存储
docker的镜像是多层存储的,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。
3. 常用的指令
常用的指令包括拉取镜像、删除镜像、创建容器、进入容器进行修改、删除容器...
docker pull ubuntu
docker image ls -a
# 查看镜像、容器数据卷所占用的空间
docker system df
# 删除无用的镜像,比如none虚悬镜像
docker image prune
# 删除本地镜像
docker image rm [选项] <镜像1> [<镜像2> ...]
<镜像> 可以是 镜像短 ID 、 镜像长 ID 、 镜像名 或者 镜像摘要
# 运行容器
# -p 81:80表示将内部的80端口映射到外部的81端口,之后可以通过访问 http://localhost:81 看到结果
docker run --name webserver -d -p 81:80 nginx
# 进入容器并修改
docker exec -it webserver bash
root@3729b97e8226:/# echo '
Hello, Docker!
' > /usr/share/nginx/html/index.html
root@3729b97e8226:/# exit
exit
# 查看容器的改动
docker diff webserver
# 查看容器
docker container ls -a
# 删除容器
docker container rm trusting_newton
#如果要删除一个运行中的容器,可以添加 -f 参数
# 清理所有处于终止状态的容器
docker container prune
4. Docker镜像制作
自己得到新镜像的方式有两种,一种是commit方式一种是使用dockerfile方式。其中前者不推荐,但是可以简单的介绍一下。
在本地修改了一个正在运行的容器之后(比如在上面的代码中的“进入容器并修改”),可以通过下面的代码保存这个容器为新的镜像
docker commit \
--author "Tao Wang " \
--message "修改了默认网页" \
webserver \
nginx:v2
通过docker image ls可以查到到新的镜像。
慎用commit使用 docker commit 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。仔细观察之前的 docker diff webserver 的结果,你会发现除了真正想要修改的 /usr/share/nginx/html/index.html文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,如果不小心处理,将会导致镜像极为臃肿。
二、正确的定制镜像的方式
正如上面提到的,正确的方式应该是Dockerfile方式,这种方式可以简单的概括为两步:
写一个Dockerfile文件。对的,文件名就叫Dockerfile,无后缀名称。把这个文件放在项目的目录中,这个文件中包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
使用docker build 镜像名称 上下文路径的方式构建镜像。
下面进入第一步,先放上一个同事的镜像(代码来自fan),然后一步步的根据这个镜像学习里面的常用docker指令,以及一些小的初学者(就是我)要记住的知识点。
from ubuntu:18.04
MAINTAINER fan dingling@dinglingdingling.cn
ARG ANTISPAM_HOME
ARG PORT
ENV LC_ALL=zh_CN.UTF-8
COPY ${ANTISPAM_HOME:-.}/dashboard /usr/lib/antispam/dashboard
COPY ${ANTISPAM_HOME:-.}/bin /usr/lib/antispam/bin
COPY ${ANTISPAM_HOME:-.}/requirements /usr/lib/antispam/requirements
EXPOSE ${PORT}
RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
RUN apt-get update
RUN apt-get install -y language-pack-zh-hans
RUN apt-get install -y python3-pip
RUN pip3 install -i http://pypi.douban.com/simple -r /usr/lib/antispam/requirements --trusted-host pypi.douban.com
ENTRYPOINT ["/usr/lib/antispam/bin/start-dashboard.sh"]
二一、常用的dockerfile指令以及编写注意事项
上下文环境的概念
1. FROM 需要修改的镜像名称
这个必须是第一条指令,指明了需要修改的镜像。如果本地不存在,则默认会去Docker Hub下载指定镜像。这里我们直接用ubuntu虚拟机。
2. MAINTAINER 标明作者
该信息将会写入生成镜像的Author属性域中。这个就像我们在写java或者python代码的时候,需要注明作者一样一样的。后面创建镜像的时候会提到这个指令的一个重要作用
3. ARG: 定义构建过程中需要的参数
用户在使用docker build构建镜像的时候,需要以--build-arg ANTISPAM_HOME=XXX --build-arg PORT=XXX的形式这些参数传入,否则会报错哦。当然也可以设置一个默认值。
ARG a_nother_name=a_default_value # 指定默认值。
如果在Dockerfile中,ARG指令定义参数之前,就有其他指令引用了参数,则参数值为空字符串。
不建议在构建的过程中,以参数的形式传递保密信息,如key, password等。
4. ENV: 在镜像的构建过程中设置环境变量
后续RUN可以使用这些环境变量,生成了镜像,运行成容器之后,这个环境变量也会被保存在env中。
未来以这个镜像为基础镜像再构建一个新的镜像v2,那么v2也会拥有该环境变量.
通过ENV定义的环境变量不能被后面的CMD指令使用,也不能被docker run 的命令参数引用。
如果不想再dockerfile中写这个环境变量,同时自己的记性足够的好,那么可以在创建容器的时候使用docker run -it -e "env_1=hahaha" 镜像名的方式传入环境变量。
5. EXPOSE :暴露端口,非必须
指明这个容器要暴露出来的内部端口,然后系统可以通过docker run -p 内部端口:外部端口的方式访问这个暴露的端口,不是必须的,没有这个EXPOSE指令,照样可以通过 -p命令暴露端口。
6. WORKDIR: 设置工作目录
设置Dockerfile的任何RUN/CMD/ENTRPOINT,COPY,ADD指令的工作目录。
这个指令虽然这里没有用到,但是也挺常用的。当使用相对目录的情况下,采用上一个WORKDIR指定的目录作为基准。举个例子,我想要封装的项目是projectA,那么我希望在docker的虚拟环境中,也有这样一个目录叫projectA,于是我可以使用这个指令创建这个目录,然后将我project中的代码一股脑的扔到这个路径中(前面提到了这会成为COPY和ADD指令的工作目录):
WORKDIR /projectA
ADD . /projectA
# 上面一句和下面一句是同一个意思,想一想是为啥
# ADD . .
相当与cd 命令,但不同的是指定了WORKDIR后,容器启动时执行的命令会在该目录下执行.
7. ADD&©:同样是将本地的文件/目录拷贝到docker中
区别在于copy不会对压缩文件进行解压,也不能通过url链接进行拷贝。一般的语法为ADD [本地路径/文件] [虚拟环境路径]
需要注意本地的路径一定要在docker的上下文环境中。 对于 COPY 和 ADD 命令来说,如果要把本地的文件拷贝到镜像中,那么本地的文件必须是在上下文目录中的文件。其实这一点很好解释,因为在执行 build 命令时,docker 客户端会把上下文中的所有文件发送给 docker daemon。考虑 docker 客户端和 docker daemon 不在同一台机器上的情况,build 命令只能从上下文中获取文件。如果我们在 Dockerfile 的 COPY 和 ADD 命令中引用了上下文中没有的文件,就会收到“no such file or directory”报错。
8. RUN: 运行指令,比如apt-get install一些软件啥的
RUN语句一般有两种使用方式
RUN 在shell中使用/bin/sh作为执行器执行命令
RUN ["executable", "param1", "param2"] 调用申明exec执行命令,比如“/bin/bash”(注意要使用双引号)RUN ["/bin/bash","-c","echo hello"]
每一个RUN语句都是在当前的基础上执行指定命令,并提交为新的镜像。当命令较长时,可以使用\来换行。如果想要在一个RUN中跑很多个任务,那可以使用 &&连接。
来看看同事的这段代码中,使用RUN做了什么
RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list 更换ubuntu的源为国内源,加快下载的速度。 sed是文件处理工作,主要以行为单位进行处理,可以将数据行进行替换、删除、新增、选取等特定工作。sed -i 's/要替换的字符串/更新的字符串/g' filename命令的作用是替换文件。
RUN apt-get update 下载器的更新,没啥好说的
RUN apt-get install -y language-pack-zh-hans 下载并安装简体中文包
RUN apt-get install -y python3-pip 安装pip3
RUN pip3 install -i http://pypi.douban.com/simple -r /usr/lib/antispam/requirements --trusted-host pypi.douban.com使用pip3的下载requirements文件中的python依赖包,同时指定使用国内的pip源(这样速度会快很多,超级赞)
所以走到这一步的时候,同事已经完成了python环境的配置啦。
如果对requirements文件是咋么生成的有疑惑的童鞋可以看下面的附录,考虑到不是docker的主要内容,正文里面就不详细介绍啦。
9. ENTRYPOINT&&CMD:都是在docker镜像启动时,执行命令。
这个语句感觉上已经和docker 镜像制作没啥关系了,这个指定docker run起来的时候,开机执行的语句。
类似于RUN命令,这里也有两种使用方式:
CMD [command]使用exec模式
CMD "command" 使用shell模式
这里要大写的注意!
使用 shell 模式时,docker 会以 /bin/sh -c "command" 的方式执行任务命令。 这意味着容器的1 号进程不是任务进程而是 bash 进程,这样我们执行的命令或者脚本可以取到环境变量。
使用exec模式,会将command的语句指定为1号进程。这意味着,如果你开启了这个镜像的容器,然后使用docker exec 容器名 ps aux去查看,会发现该命令的进程为1. exec 模式是建议的使用模式,因为当运行任务的进程作为容器中的 1 号进程时,我们可以通过 docker 的 stop 命令优雅的结束容器.
exec 模式的特点是不会通过 shell 执行相关的命令,所以像 $HOME 这样的环境变量是取不到的:
FROM ubuntu
CMD [ "echo", "$HOME" ]
# 如果想要取到,需要使用这样的表示,指定执行shell来获得
FROM ubuntu
CMD [ "sh", "-c", "echo $HOME" ]
二二、写自己的镜像文件Dockerfile
我需要封装的是一个python项目,可以使用CPU,也可以使用GPU,处于简单的角度考虑,我先写一个CPU版本的。总结如下:
镜像源是ubuntu:18.04
指定workdir
将本地的项目文件拷贝到镜像的项目目录中
更改ubuntu的源路径
更新apy-get update
下载一些依赖
下载简体中文包
安装python3-pip
安装TensorFlow1.10.0的二进制版本(pip默认安装的tensorflow版本没有对AVX指令集进行支持)
将项目的其他依赖包放在requirements文件中,使用pip3下载安装
FROM ubuntu:18.04
MAINTAINER wenting
LABEL version="1.0"
WORKDIR /chineseocr_api
ADD . /chineseocr_api
RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
RUN rm -rf /var/lib/apt/lists/* # 如果修改加速镜像之后,发现docker还是从源路径拉取安装包,那么这句话就很重要了
RUN apt-get update
RUN apt-get install libsm6 libxrender1 libxext-dev gcc -y
RUN apt-get install -y language-pack-zh-hans
RUN apt-get install -y python3-pip
RUN pip3 install tensorflow-1.10.0-cp36-cp36m-linux_x86_64.whl
RUN pip3 install -i http://pypi.douban.com/simple -r /chineseocr_api/requirements --trusted-host pypi.douban.com
二三、Docker build构建镜像
这个指令挺简单的 给出想要命名的镜像名称和创建的上下文环境
sudo docker build -t chineseocr_api/v1 .
如果在build的过程中出错了,不用担心错处语句前面的工作都白做了。因为docker build语句使用了缓存机制。
Dockerfile的每条指令都会将结果提交为新的镜像。下一条指令基于上一条指令的镜像进行构建。也就是你修改Dockerfile后,build任务会快速略过你之前成功的步骤,从你修改的那一步之后的操作,都会重新运行。因此,为了有效的利用缓存,尽量保持Dockerfile一致,并且尽量在末尾修改。
更改 MAINTAINER 指令会使Docker强制执行 run 指令来更新apt,而不是使用缓存。
二四、 运行一下我们创建的镜像
此时,镜像是在本地的。可以通过docker images ls来查看镜像。
然后就可以使用下面的命令运行这个镜像。
sudo docker run --name chineseocr_api -p 7070:7070 chineseocr_api/v1 python3 server.py
这里使用--name为容器命令,使用-p 7070:7070指定容器内端口和外部端口的对应关系。 python server.py表示开启容器之后,运行/bin/sh -c python3 server.py 这样我的服务就以docker的形式完成的封装以及提供了服务。大家可以通过访问 http://hostname:7070来提交查询。
参考资料
附录:
A. requirements是如何生成的
在写python项目的时候,推荐的使用conda构建自己的python依赖环境,这样可以通过conda list -e>requirements的方式,将依赖导出,导出的形式是这样的:
readline==7.0
requests==2.22.0
scipy==1.3.0
setuptools==41.0.1
setuptools==39.1.0
······
但是这里需要注意的是,这种导出方式导出的只是使用conda install下载的依赖。
如果是使用pip下载的依赖,那么可以使用pip freeze>requirements的方式导出依赖。
这样导出了之后,可以通过pip install -r requirement的方式批量下载依赖。
最后
如果在docker容器封装中,遇到问题,可以留言,我会定期查看留言,如果有我能解决的也会和你讨论一下。共同学习共同进步~