为Python应用构建最精简Docker

创建一个Docker容器时最好将镜像保持最小的状态。这样通常可以使构建和部署容器更快。

每个容器应该包含应用程序代码,特定于语言的依赖项,操作系统依赖项。任何其他的多余指令不仅使镜像变得更加臃肿也创造了很多潜在的安全问题。如果在部署到生产环境的容器中有像gcc这样的工具,那么具有shell访问权限的攻击者可以轻松地构建工具来访问其他内部系统。因此需要构建多层防御层来最大限度地减少攻击造成的伤害。

我最近在研究Python网络服务器。
requirements.txt文件大多是:

Flask>=0.12,<0.13 
flask-restplus>=0.9.2,<0.10 
Flask-SSLify>=0.1.5,<0.2 
Flask-Admin>=1.4.2,<1.5 
gunicorn>=19,<20

如今普通Dockerfiles的大小大都在500MB以上,很难想象一个普通的Python应用竟占用如此大小,如果忽视这个问题,其终究会变成Python发展的隐患。

精简在程序开发中很重要,但太小也可能有害。我们可以重构所有容器,但事实上我们很难忽视时间的成本。值得尊敬的是并不是所有的程序都很臃肿,比如alpine,该映像的大小仅为5 MB。您还可以使用最小的POSIX环境来构建应用程序。

FROM python:3.7-alpine
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["gunicorn", "-w 4", "main:app"]

此容器占用大小为114MB。其中,基本图像为86.7MB(在写入时)。这意味着我们的应用程序负责额外的27.3MB。
注意:Alpine默认使用musl而不是glibc。这意味着如果不强制重新编译,一些Python程序将无法使用。

每次我们对源代码进行更改或者重建容器时,都会重新下载并重新安装依赖项。这显然是极大地浪费,进行迭代式开发需要太多时间。所以我们需要重写Dockerfile来利用缓存。

FROM python:3.7-alpine
COPY requirements.txt /
RUN pip install -r /requirements.txt
COPY src/ /app
WORKDIR /app
CMD ["gunicorn", "-w 4", "main:app"]

这种方式重写我们的Dockerfile会使用Docker的层缓存,并且如果requirements.txt文件没有更改,则会跳过安装Python依赖。这就可以使加快我们的构建速度,但它对整体图像大小没有影响。

如果仔细观察上面的Docker构建输出,可以看到以下内容:

Building wheels for collected packages: Flask-SSLify, Flask-Admin, itsdangerous, wtforms, MarkupSafe
Running setup.py bdist_wheel for Flask-SSLify: started
Running setup.py bdist_wheel for Flask-SSLify: finished with status ‘done’
Stored in directory: /root/.cache/pip/wheels/70/14/5b/fbd15774657c5cadc661a66236d121640c60dd9382f2a28469
Running setup.py bdist_wheel for Flask-Admin: started
Running setup.py bdist_wheel for Flask-Admin: finished with status ‘done’
Stored in directory: /root/.cache/pip/wheels/3f/0f/33/5e27d4e7ba9459198695c28f879659197e33be5d5338a07a1b
Running setup.py bdist_wheel for itsdangerous: started
Running setup.py bdist_wheel for itsdangerous: finished with status ‘done’
Stored in directory: /root/.cache/pip/wheels/fc/a8/66/24d655233c757e178d45dea2de22a04c6d92766abfb741129a
Running setup.py bdist_wheel for wtforms: started
Running setup.py bdist_wheel for wtforms: finished with status ‘done’
Stored in directory: /root/.cache/pip/wheels/36/35/f3/7452cd24daeeaa5ec5b2ea13755316abc94e4e7702de29ba94
Running setup.py bdist_wheel for MarkupSafe: started
Running setup.py bdist_wheel for MarkupSafe: finished with status ‘done’
Stored in directory: /root/.cache/pip/wheels/88/a7/30/e39a54a87bcbe25308fa3ca64e8ddc75d9b3e5afa21ee32d57

当pip install运行时,它还存储了我们下载到/root/.cache的依赖项的备份。当我们在Docker之外使用本地开发时,这些确实有用。但是,这个目录占用了我们27.3MB的空间,'app'占用7MB。。。。我们可以通过利用Docker另一个功能 - 多级构建来解决这一点。

Docker 17.05增加了对多级构建的支持。这意味着可以在一个映像中构建依赖项,然后可以将其导入另一个映像。
现在重写我们的Dockerfile以使用多级构建:

FROM python:3.7-alpine as base
FROM base as builder
RUN mkdir /install
WORKDIR /install
COPY requirements.txt /requirements.txt
RUN pip install --install-option="--prefix=/install" -r /requirements.txt
FROM base
COPY --from=builder /install /usr/local
COPY src /app
WORKDIR /app
CMD ["gunicorn", "-w 4", "main:app"]

这个Docker容器大小为103MB,编译的Python依赖项的大小为21.9M(我通过在构建容器中运行du -h / install来解决这个问题)。
现在我们仍可以进一步压缩其大小,比如在容器构建时删除无关文件,如文档,测试等。但是要做好被打的准备----手动滑稽 :huaji3:

点赞
  1. emetechnologies说道:

    great post.very useful information.

发表评论

电子邮件地址不会被公开。必填项已用 * 标注