Dockerfile封装实战学习——python项目

今天项目需要将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 '<h1>Hello, Docker!</h1>' > /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 <twang2218@gmail.com>" \
--message "修改了默认网页" \
webserver \
nginx:v2

通过docker image ls可以查到到新的镜像。

慎用commit使用 docker commit 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。仔细观察之前的 docker diff webserver 的结果,你会发现除了真正想要修改的 /usr/share/nginx/html/index.html文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,如果不小心处理,将会导致镜像极为臃肿。

二、正确的定制镜像的方式

正如上面提到的,正确的方式应该是Dockerfile方式,这种方式可以简单的概括为两步:

  1. 写一个Dockerfile文件。对的,文件名就叫Dockerfile,无后缀名称。把这个文件放在项目的目录中,这个文件中包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
  2. 使用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&&COPY:同样是将本地的文件/目录拷贝到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语句一般有两种使用方式

  1. RUN <COMMAND> 在shell中使用/bin/sh作为执行器执行命令
  2. RUN ["executable", "param1", "param2"] 调用申明exec执行命令,比如“/bin/bash”(注意要使用双引号)RUN ["/bin/bash","-c","echo hello"]

每一个RUN语句都是在当前的基础上执行指定命令,并提交为新的镜像。当命令较长时,可以使用\来换行。如果想要在一个RUN中跑很多个任务,那可以使用 &&连接。

来看看同事的这段代码中,使用RUN做了什么

  1. RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list 更换ubuntu的源为国内源,加快下载的速度。 sed是文件处理工作,主要以行为单位进行处理,可以将数据行进行替换、删除、新增、选取等特定工作。sed -i 's/要替换的字符串/更新的字符串/g' filename命令的作用是替换文件。
  2. RUN apt-get update 下载器的更新,没啥好说的
  3. RUN apt-get install -y language-pack-zh-hans 下载并安装简体中文包
  4. RUN apt-get install -y python3-pip 安装pip3
  5. 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命令,这里也有两种使用方式:

  1. CMD [command]使用exec模式
  2. 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版本的。总结如下:

  1. 镜像源是ubuntu:18.04
  2. 指定workdir
  3. 将本地的项目文件拷贝到镜像的项目目录中
  4. 更改ubuntu的源路径
  5. 更新apy-get update
  6. 下载一些依赖
  7. 下载简体中文包
  8. 安装python3-pip
  9. 安装TensorFlow1.10.0的二进制版本(pip默认安装的tensorflow版本没有对AVX指令集进行支持)
  10. 将项目的其他依赖包放在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来提交查询。

参考资料

Dockerfile最佳实践

Dockerfile中的ARG指令详解

dockerfile之ENV指令

dockerfile之WORKDIR指令

附录:

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容器封装中,遇到问题,可以留言,我会定期查看留言,如果有我能解决的也会和你讨论一下。共同学习共同进步~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容

  • 让博客Docker化,轻松上手Docker Docker是一个有趣的技术,在过去的两年已经从一个想法变成了全世界的...
    喵喵唔的老巢阅读 326评论 0 0
  • docker基本概念 1. Image Definition 镜像 Image 就是一堆只读层 read-only...
    慢清尘阅读 8,719评论 1 21
  • 一、Docker创建镜像的方式有两种: 一种通过commit的方式:把做了一系列操作的容器关闭,然后利用docke...
    jie0112阅读 3,818评论 0 3
  • 在前面两节我们学习了如何安装以及简单的运行管理docker容器,在本节我们将会更多的探讨关于docker镜像的知识...
    井底蛙蛙呱呱呱阅读 4,075评论 0 5
  • 1.怎么保证多人开发进行内存泄露的检查. 1>使用Analyze进行代码的静态分析 2>为避免不必要的麻烦,多人开...
    简了个书呗阅读 310评论 0 0