Docker学习笔记(五)
自制Docker镜像
一般的,当我们的程序开发完成后,会连同程序文件与运行环境一起制作成一个新的镜像。
要制作镜像,需要编写Dockerfile。DockeFile由多个命令组成,常用的命令有:
- FROM:基于某个镜像来制作新的镜像。格式为:FROM 镜像名称:镜像版本。
- COPY:从宿主机复制文件,支持
?
、*
等通配符。格式为:COPY 源文件路径 目标文件路径。 - ADD:从宿主机添加文件,格式与COPY相同,区别在于当文件为压缩文件时,会解压缩到目标路径。
- RUN:在创建新镜像的过程中执行的shell命令。格式为:RUN shell命令行。注意,此shell命令将在容器内执行。
- CMD:在容器实例中运行的命令,格式与RUN相同。注意,如果在docker run时指定了命令,将不会执行CMD的内容。
- ENTRYPOINT:在容器实例中运行的命令,格式与CMD相同。注意,如果在docker run时指定了命令,该命令会以命令行参数的形式传递到ENTRYPOINT中。
- ENV:在容器中创建环境变量,格式为:ENV 变量名 值
注意,Docker镜像中有一个层的概念,每执行一个RUN命令,就会创建一个层,层过多会导致镜像文件体积增大。尽量在RUN命令中使用&&
连接多条shell命令,减少RUN命令的个数,可以有效减小镜像文件的体积。
自制显示文本文件内容镜像
编写cat.py,接收一个文件名,由python读取文件并显示文件的内容:
import os
import sys
input = sys.argv[1]
with open(input, "r") as fp:
print(fp.read())
这个例子比较简单,缩写Dockerfile如下:
FROM python:3.8
WORKDIR /files
COPY cat.py /cat.py
ENTRYPOINT ["python", "/cat.py"]
这个Dockerfile的含义是:
- 以python:3.8为基础镜像
- 容器启动命令的工作目录为/files,在运行镜像时,需要我们把宿主机的某目录挂载到容器的/files目录
- 复制cat.py到容器的根目录下
- 启动时运行python /cat.py命令
需要说明的是,ENTRYPOINT有两种写法:
ENTRYPOINT python /cat.py
ENTRYPOINT ["python", "/cat.py"]
这里采用第二种写法,是因为我们要在外部给容器传递参数。执行命令编译Docker镜像:
docker build -t cat:1.0 .
这个命令中,-t的含义是目标,即生成的镜像名为hello,版本号为1.0,别忘了最后那个.
,这叫到上下文路径,是指 docker 在构建镜像,有时候想要使用到本机的文件(比如复制),docker build 命令得知这个路径后,会将路径下的所有内容打包。
这样,我们的第一个镜像就制作完成了,使用下面的命令执行它:
docker run -it -v ~/docker_test/cat/files:/files cat:1.0 test.txt
即可看到~/docker_test/cat/files/test.txt的内容。
自制web服务器镜像
我们使用tornado开发一个网站,而python的官方镜像是没有tornado库的,这就需要在制作镜像时进行安装。
测试的ws.py如下:
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)
class IndexHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello world")
if __name__ == "__main__":
tornado.options.parse_command_line()
app = tornado.web.Application(handlers=[(r"/", IndexHandler)])
http_server = tornado.httpserver.HTTPServer(app)
http_server.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
编写Dockerfile文件如下:
FROM python:3.8
WORKDIR /ws
COPY ws.py /ws/ws.py
RUN pip install tornado
CMD python hello.py
在此我们验证一下CMD与ENTRYPOINT的区别。在Dockerfile所在有目录下执行如下命令:
docker build -t ws:1.0 .
执行完成后,再使用docker images
使用就可以看到生成的镜像了,然后使用下面的命令运行:
docker run -it -p 8000:8000 ws:1.0
在浏览器中输入宿主机的ip和8000端口,就可以看到页面了。
在这个例子中,我使用的运行命令是CMD
,如果在docker run中指定的其他的命令,此命令就不会被执行,如:
$ docker run -it -p 8000:8000 ws:1.0 python
Python 3.8.7 (default, Dec 22 2020, 18:46:25)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
此时,容器中被执行的是python命令,而不是我们的服务。在更多情况下,我们希望在docker run命令中为我们的服务传参,而不是覆盖执行命令,那么,我们应该使用ENTRYPOINT
而不是CMD
:
FROM python:3.8
WORKDIR /ws
COPY ws.py /ws/ws.py
RUN pip install tornado
ENTRYPOINT python ws.py
上面这种写法,是不支持传递参数的,ENTRYPOINT
和CMD
还支持另一种写法:
FROM python:3.8
WORKDIR /ws
COPY ws.py /ws/ws.py
RUN pip install tornado
ENTRYPOINT ["python", "ws.py"]
使用这种写法,docker run命令中的参数才可以传递给hello.py:
docker run -it -p 8000:9000 ws:1.0 --port=9000
这个命令中,--port=9000
被作为参数传递到hello.py中,因此容器内的端口就成了9000。
在生产环境中运行时,不会使用-it选项,而是使用-d选项,让容器在后台运行:
$ docker run -d -p 8000:9000 ws:1.0 --port=9000
4a2df9b252e2aff6a8853b3a8bf46c0577545764831bb7557b836ddcd85cba70
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4a2df9b252e2 hello:1.0 "python ws.py --p…" 9 seconds ago Up 8 seconds 0.0.0.0:8000->9000/tcp elegant_sammet
这种方式下,即使当前的控制台被关闭,该容器也不会停止。
自制apscheduler服务镜像
接下来,制作一个使用apscheduler编写的服务镜像,代码如下:
import sys
import shutil
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
def scan_files():
shutil.copytree(sys[1], sys[2])
scheduler = BlockingScheduler()
scheduler.add_job(
scan_files,
trigger=CronTrigger(minute="*"),
misfire_grace_time=30
)
Dockerfile也是信手拈来:
FROM python:3.8
WORKDIR /
COPY sch.py /sch.py
RUN pip install apscheduler
ENTRYPOINT ["python", "sch.py"]
生成镜像:
docker build -t sch:1.0 .
应该可以运行了,文件复制需要两个目录,在运行时,可以使用两次-v来挂载不同的目录:
docker run -d -v ~/docker_test/sch/src:/src -v ~/docker_test/sch/dest:/dest sch:1.0 /src /dest
减小镜像体积
前面用到的官方python镜像大小足足882MB,在这个基础上,再安装用到的第三方库,添加项目需要的图片等资源,大小很容易就超过1个G,这么大的镜像,网络传给客户非常的不方便,因此,减小镜像的体积是非常必要的工作。
docker hub上有个一python:3.8-alpine镜像,大小只有44.5MB。之所以小,是因为alpine是一个采用了busybox架构的操作系统,一般用于嵌入式应用。我尝试使用这个镜像,发现安装一般的库还好,但如果想安装numpy等就会困难重重,甚至网上都找不到解决方案。
还是很回到基本的路线上来,主流的操作系统镜像,ubuntu的大小为72.9MB,centos的大小为209MB,真是让人感慨差距怎么那么大呢!使用ubuntu作为基础镜像,安装python后的大小为139MB,再安装pip后的大小一下子上升到了407MB,这个大小,再安装点别的还真就能达到python官方镜像的大小了。
不过,还有一条曲线救国的路,叫到——多阶段构建。
多阶段构建
多阶段构建的思想其实很简单,先构建一个大而全的镜像,然后只把镜像中有用的部分拿出来,放在一个新的镜像里。在我们的场景下,pip只在构建镜像的过程中需要,而对运行我们的程序却一点用处也没有。我们只需要安装pip,再用pip安装第三方库,然后将第三方库从这个镜像中复制到一个只有python,没有pip的镜像中,这样,pip占用的268MB空间就可以被节省出来了。
-
在ubuntu镜像的基础上安装python:
FROM ubuntu RUN apt update \ && apt install python3
然后运行:
docker build -t python:3.8-ubuntu .
这样,就生成了python:3.8-ubuntu镜像。
-
在python:3.8-ubuntu的基础上安装pip:
FROM python:3.8-ubuntu RUN apt install python3
然后运行:
docker build -t python:3.8-ubuntu-pip .
这样,就生成了python:3.8-ubuntu-pip镜像。
-
多阶段构建目标镜像:
FROM python:3.8-ubuntu-pip RUN pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple numpy FROM python:3.8-ubuntu COPY --from=0 /usr/local/lib/python3.8/dist-packages/ /usr/local/lib/python3.8/dist-packages/
这个dockerfile需要解释一下了,因为它有两个FROM命令。
第一个是以python:3.8-ubuntu-pip镜像为基础,安装numpy,当然,在实际应用中,把所有用到的第三方库出写在这里。
第二个FROM是以FROM python:3.8-ubuntu镜像为基础,将第三方库统统复制过来,COPY命令后的–from=0的意思是从第0阶段进行复制。实际应用中再从上下文中复制程序代码,添加需要的ENTRYPOINT等。
最后,再运行:
docker build -t project:1.0 .
这然,用于我们项目的镜像就做好了。比使用官方python镜像构建的版本,小了大约750MB。
导入镜像到生产环境
到此,我们的镜像已经制作好了,可是,镜像文件在哪,如何在生产环境下运行呢?
刚才使用docker images命令时,已经看到了生成的镜像:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello 1.0 01fe19111dc7 59 minutes ago 893MB
python 3.8 f5041c8ae6b1 13 days ago 884MB
ubuntu 20.04 f643c72bc252 5 weeks ago 72.9MB
hello-world latest bf756fb1ae65 12 months ago 13.3kB
我们可以使用docker save
命令将镜像保存到指定的文件中,保存的文件是一个.tar格式的压缩文件:
docker save -o hello.tar hello:1.0
将hello.tar复制到生产环境的机器上,然后执行导入命令:
docker load -i hello.tar
就可以使用了。
- 点赞
- 收藏
- 关注作者
评论(0)