Docker学习笔记(五)

举报
天元浪子 发表于 2021/11/25 16:15:37 2021/11/25
【摘要】 Docker是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。

自制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的含义是:

  1. 以python:3.8为基础镜像
  2. 容器启动命令的工作目录为/files,在运行镜像时,需要我们把宿主机的某目录挂载到容器的/files目录
  3. 复制cat.py到容器的根目录下
  4. 启动时运行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

上面这种写法,是不支持传递参数的,ENTRYPOINTCMD还支持另一种写法:

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空间就可以被节省出来了。

  1. 在ubuntu镜像的基础上安装python:

    FROM ubuntu
    RUN apt update \
        && apt install python3
    

    然后运行:

    docker build -t python:3.8-ubuntu .
    

    这样,就生成了python:3.8-ubuntu镜像。

  2. 在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镜像。

  3. 多阶段构建目标镜像:

    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

就可以使用了。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。