创建Docker容器部署Python应用程序

-1 投票
2 回答
63 浏览
提问于 2025-04-12 00:44

当前问题

我正在尝试将一个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下创建两个子目录group1group2,并在这两个组中共享公共的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文件安装到其中。

我认为这个过程应该是这样的:

  1. 将Python共享库代码构建成一个Wheel
  2. 使用Python发行版镜像作为基础创建一个Docker容器
  3. 将Wheel复制到Docker容器中
  4. 在Docker容器内安装Wheel
  5. 将剩余的可执行Python代码复制到Docker容器中(应该放在哪里?)
  6. 将容器的默认入口点设置为其中一个Python可执行文件
  7. 然后还有关于“额外功能”的问题,例如添加到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 个回答

0

我觉得这可能比我最开始想的要简单。

我认为在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的步骤也许是一个不足之处。是不是可以考虑把二进制文件安装到其他地方呢?
1

如何创建一个Docker镜像,以及如何在里面安装一个.whl文件。

你在Docker镜像里安装、构建,做所有事情。你只需要复制源代码,确保是一个全新的、没有修改过的代码库,里面没有任何新文件。这样做的目的是让它变得自包含可重复,还有其他一些流行的说法。

FROM python
COPY . .
RUN pip install -e .

完成了。通常会先安装requirements.txt,这样可以把依赖项缓存到Docker的层中,以便加快构建速度。

我对poetry不太了解,pip是我常用的标准工具。关于poetry,按照文档的说法,想要进行可编辑安装可以用poetry add --editable .

撰写回答