创建Docker容器部署Python应用程序
当前问题
我正在尝试将一个Python应用程序投入生产使用。这并不简单,因为Python不支持将源代码打包成一个单独的可执行文件。此外,Python也没有一个自然的方式来创建共享库。接下来我会详细解释这个问题。
Python和共享库
大多数编程语言都有一些相对方便的方法来将代码投入生产。这通常包括:
- 创建一些包含项目公共代码的共享库
- 创建可执行文件,这些文件使用共享库(可能还有其他库)
举几个例子:
- Java可以将代码打包成一个
.jar
文件。还有像Maven和Gradle这样的工具来帮助部署和打包。 - 对于C++和C等语言,通常会构建二进制库和二进制可执行文件。这些可以被部署。还有一种方法可以将二进制可执行文件链接到所有共享库代码。
- Rust有Cargo,通常会构建包含所有相关代码的独立二进制可执行文件。
Python的工作方式有点不同,因为解释器只知道当前工作目录的路径和添加到PYTHONPATH
环境变量中的路径。
- 修改
PYTHONPATH
环境变量通常不太建议,因为这不容易扩展,并且需要额外的维护工作。
这意味着要么:
- Python的可执行文件必须是一个单独的文件,且可执行文件之间不能有共享代码
- 或者,任何共享代码必须放在一个库中,并且这个库必须放在与可执行文件相同的当前工作目录下
这就建议了一个这样的项目结构:
my-project/
bin/
executable1.py
executable2.py
...
lib1/
__init__.py
...
lib2/
__init__.py
...
显然,我们不会这样构建项目。把所有共享库代码放在与可执行文件相同的目录下已经很奇怪了。如果我们想要在bin
下有多个目录来存放不同“组”的可执行文件,这种结构就会崩溃。例如,无法在bin
下创建两个子目录group1
和group2
,并在这两个组中共享公共的Python代码。
这就是我说Python不支持共享库的原因。你可以构建一个共享库,但你需要使用其他工具来完成这件事。(除非你选择修改PYTHONPATH
,但我们假设不想这样做。)
解决Python共享库问题的方法
实际上,我们会使用像virtualenv(venv)或Poetry(本质上是管理虚拟环境的工具)来将公共库代码放到其他目录,并让Python解释器能够找到它。
我当前的工作流程情况
这意味着我有这样的项目结构:
my-project
.venv/...
bin/
...
src/
lib1/...
lib2/...
pyproject.toml
而且我一直在使用交互模式的venv:
$ .venv/bin/activate
$ pip3 install -e .
这对于开发来说非常好,因为如果src
下的库代码发生变化,这些变化会“实时”显示出来。(意思是没有打包和安装的步骤。只需运行可执行文件,当前的代码就会被使用。)
但这对于部署来说就不太好。
为什么选择Docker?
我最初尝试“安装”一个生产就绪版本的代码的方法是:
- 使用
systemd
来管理进程(启动和停止) - 将可执行代码从
bin
文件夹复制到系统上的某个“部署”位置(例如/opt/my-project/bin/
) - 构建一个wheel(
.whl
)文件 - 在系统范围内安装
whl
我没有完成最后两个步骤,因为我意识到这可能不是一个好方法。这有一些问题:
- 使用虚拟环境的目的是为了避免在系统范围内使用
pip
安装包。因此,创建一个whl
并在系统范围内安装并没有太大意义。 - 我也不知道在这种情况下如何使用虚拟环境。我描述的是将可执行的Python文件复制到像
/opt
这样的目录,而这个目录显然在包含.venv
的“开发”目录之外。 - 这个想法似乎没有太多意义。
这让我相信使用Docker会是一个更合理的方法。我们可以在Docker容器内全局安装whl
。
- 经过反思,我实际上认为没有使用Docker就无法合理地将Python代码投入生产和部署。如果有人对此有任何想法,欢迎反馈。
Docker方法和当前问题
我目前不明白如何创建Docker镜像以及如何将whl
文件安装到其中。
我认为这个过程应该是这样的:
- 将Python共享库代码构建成一个Wheel
- 使用Python发行版镜像作为基础创建一个Docker容器
- 将Wheel复制到Docker容器中
- 在Docker容器内安装Wheel
- 将剩余的可执行Python代码复制到Docker容器中(应该放在哪里?)
- 将容器的默认入口点设置为其中一个Python可执行文件
- 然后还有关于“额外功能”的问题,例如添加到
PATH
或可能重命名一个.py
可执行文件,使其看起来像一个常规可执行文件……也许?
我感到困惑的原因是我在使用Poetry来管理开发过程中的虚拟环境(这些在生产Docker镜像中并不需要),而我不明白如何执行上述步骤,因为Docker容器将没有venv或Poetry安装。但我在开发过程中使用这两者来运行我的代码。(在Docker容器外。)
这是我现在在pyproject.toml
中有的内容:
[tool.poetry]
name = "docker-python-poetry-example"
version = "0.1.0"
description = ""
authors = ["Example <example@example.com>"]
readme = "README.md"
[tool.poetry.scripts]
example-executable = 'bin.example_executable:main'
[tool.poetry.dependencies]
python = "^3.11"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
我感到困惑,因为我不明白我应该在哪里以及何时使用Poetry。Docker容器需要安装Poetry才能安装wheel吗?我认为不应该有这个。
2 个回答
我觉得这可能比我最开始想的要简单。
我认为在Docker容器外需要用到Poetry,而在容器内部就不需要它了。
这是我想到的解决方案。
首先,需要执行的命令顺序是:
# build the .whl using Poetry
poetry build
# build the Docker container from the Dockerfile
docker build -t example-container .
这是更新后的Dockerfile
:
from python:3.12-bookworm
workdir /opt/docker-python-poetry-example
# copy the whl, and install it system-wide, inside the Docker container
copy dist/*.whl /opt/docker-python-poetry-example
run pip install *.whl
run rm /opt/docker-python-poetry-example/*.whl
# copy the executable "binaries" (Python scripts), add this dir to `PATH`
copy bin /opt/docker-python-poetry-example/bin
run chmod +x /opt/docker-python-poetry-example/bin/*
env PATH="/opt/docker-python-poetry-example/bin:${PATH}"
# entry point
cmd example_executable.py
看起来是有效的。
我认为这是一个合理的方法,原因如下:
- 我们想要分发一些Python代码(库)
- 通常,这会通过构建一个wheel文件来完成,这个文件可以上传到pypy供其他人使用,或者直接复制到目标机器上用pip安装
- 在这种情况下,我们把wheel文件和可执行的Python“二进制文件”一起打包在Docker容器镜像里
- Docker镜像易于分发、启动和停止。它是一个独立的“东西”或“包”
我认为的不足之处:
- 库代码和二进制文件的分发方式是不同的
- 理想情况下,应该有一个统一的方法来对待这两种代码元素
- 库是通过wheel文件分发的
- 而二进制代码只是简单地复制过来
我个人认为,把库和二进制文件区分对待是一个不足之处。
- 手动更新
PATH
的步骤也许是一个不足之处。是不是可以考虑把二进制文件安装到其他地方呢?
如何创建一个Docker镜像,以及如何在里面安装一个.whl文件。
你在Docker镜像里安装、构建,做所有事情。你只需要复制源代码,确保是一个全新的、没有修改过的代码库,里面没有任何新文件。这样做的目的是让它变得自包含、可重复,还有其他一些流行的说法。
FROM python
COPY . .
RUN pip install -e .
完成了。通常会先安装requirements.txt,这样可以把依赖项缓存到Docker的层中,以便加快构建速度。
我对poetry不太了解,pip
是我常用的标准工具。关于poetry,按照文档的说法,想要进行可编辑安装可以用poetry add --editable .
。