SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-13

تفاوت venv، pipx و uv در مدیریت پایتون روی سرور

خطای externally-managed-environment در Ubuntu 24.04 مانع نصب با pip می‌شود. با انتخاب صحیح بین venv، pipx یا uv برای پروژه‌ها و ابزارهای خود، از تداخل با بسته‌های سیستم جلوگیری کنید.

چرا دستور pip install روی یک سرور تازه Ubuntu با خطا مواجه می‌شود

انتخاب بین Python venv، pipx و uv روی یک سرور به یک پرسش اساسی برمی‌گردد: چه چیزی نصب می‌کنید؟ وابستگی‌های یک برنامه باید در یک محیط مجازی (virtual environment) درون دایرکتوری همان برنامه قرار بگیرند. ابزارهای خط فرمانی که می‌خواهید با نامشان آن‌ها را اجرا کنید، باید با pipx نصب شوند. ابزار uv هر دو کار را انجام می‌دهد و یک فایل lockfile نیز اضافه می‌کند که به محض نیاز به ساخت محیط مشابه روی ماشین دوم، اهمیت پیدا می‌کند. هیچ‌کدام از این ابزارها در Python سیستمی نصب نمی‌کنند، زیرا یک سرور Ubuntu مدرن مستقیماً با این کار مخالفت می‌کند.

دستور sudo pip install requests را روی Ubuntu 24.04 اجرا کنید تا ببینید pip پیش از دانلود حتی یک فایل، متوقف می‌شود.

error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.

    If you wish to install a non-Debian-packaged Python package,
    create a virtual environment using python3 -m venv path/to/venv.
    Then use path/to/venv/bin/python and path/to/venv/bin/pip.

    If you wish to install a non-Debian packaged Python application,
    it may be easiest to use pipx install xyz, which will manage a
    virtual environment for you.

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this behaviour by passing --break-system-packages.

این رفتار ناشی از PEP 668 (پیشنهاد بهبود پایتون 668، «محیط‌های مدیریت‌شده توسط سیستم») است که وظیفه خود را انجام می‌دهد. توزیع‌های Debian و Ubuntu یک فایل نشانگر در کنار مفسر در مسیر /usr/lib/python3.12/EXTERNALLY-MANAGED قرار می‌دهند و pip از نوشتن در هر مفسری که این فایل را داشته باشد، خودداری می‌کند.

این قانون به دلیل ترتیب sys.path وجود دارد. apt کتابخانه‌ها را در /usr/lib/python3/dist-packages نصب می‌کند. اگر pip را با دسترسی root روی مفسر سیستم اجرا کنید، در /usr/local/lib/python3.12/dist-packages می‌نویسد و بسته‌بندی Debian این دایرکتوری را در اولویت بالاتری در مسیر جستجو قرار می‌دهد. با اجرای python3 -c 'import sys; print(sys.path)' می‌توانید این ترتیب را مشاهده کنید. بنابراین، نسخه‌ای که pip نوشته است، جایگزین نسخه‌ای می‌شود که apt نصب کرده؛ این اتفاق برای تمام برنامه‌هایی که تحت /usr/bin/python3 اجرا می‌شوند رخ می‌دهد، از جمله ابزارهای خود توزیع. ابزار cloud-init کتابخانه‌های requests، jinja2 و PyYAML را از همان مفسر فراخوانی می‌کند. اگر یکی از آن‌ها را با pip ارتقا دهید و به یک نسخه ناسازگار برسید، برنامه‌ای که هرگز به آن دست نزده‌اید در بوت بعدی با یک traceback که نام بسته‌ای ناشناخته را می‌آورد، از کار می‌افتد. از آنجا که apt همچنان نسخه خود را به عنوان نصب‌شده ثبت کرده است، هیچ هشداری دریافت نمی‌کنید و تعمیر آن sudo apt reinstall python3-requests خواهد بود.

قانون حاصل بسیار ساده است: Python سیستمی متعلق به توزیع است. در آن چیزی نصب نکنید، کتابخانه‌های آن را با pip ارتقا ندهید و فایل EXTERNALLY-MANAGED را برای حذف پیام خطا پاک نکنید. تنها وظیفه‌ای که باید به /usr/bin/python3 بسپارید، ساخت محیط‌های مجازی است.

مقایسه venv در برابر pipx و uv: قاعده تصمیم‌گیری

انتخاب ابزار را بر اساس ماهیت چیزی که نصب می‌کنید انجام دهید، نه بر اساس ابزاری که اخیراً درباره آن مطالعه کرده‌اید.

  • برنامه‌ای که آن را مستقر کرده و به عنوان یک سرویس اجرا می‌کنید، مانند پروژه‌های Django یا Flask: از یک محیط مجازی (venv) در داخل دایرکتوری همان برنامه استفاده کنید.
  • ابزار خط فرمانی که می‌خواهید روی PATH خود داشته باشید، مانند ansible یا httpie: از pipx استفاده کنید؛ این ابزار برای هر برنامه یک محیط اختصاصی ایجاد کرده و یک لینک در PATH قرار می‌دهد.
  • پروژه‌ای که به lockfile، سرعت نصب بالاتر یا نسخه‌ای از Python نیاز دارد که در توزیع سیستم‌عامل موجود نیست: از uv استفاده کنید که یک venv معمولی به همراه یک فایل uv.lock تولید می‌کند.
  • کتابخانه‌ای که یک ابزار توزیع‌شده به آن نیاز دارد و نه کد شما: از sudo apt install python3-<name> استفاده کنید؛ این تنها روش پشتیبانی‌شده برای افزودن هر چیزی به مفسر سیستم است.

ابزارهای pipx و uv tool install وظیفه یکسانی دارند، بنابراین سیستمی که از قبل uv دارد، نیازی به pipx نخواهد داشت. انتخاب فریم‌ورک وب هیچ تغییری در این قاعده ایجاد نمی‌کند: Django و Flask روی یک VPS در محتوای requirements.txt تفاوت دارند، نه در نحوه ساخت محیط پیرامون آن. تمام موارد زیر از Ubuntu 24.04 و Python 3.12 استفاده می‌کنند، بنابراین اگر نسخه شما متفاوت است، شماره نسخه را در مسیرها اصلاح کنید.

ساخت venv برای هر برنامه

توزیع Ubuntu ماژول venv را از بسته اصلی Python جدا کرده است، بنابراین در یک image حداقلی، اولین تلاش با پیامی که دقیقاً نام بسته مفقود را اعلام می‌کند، با شکست مواجه می‌شود.

The virtual environment was not created successfully because ensurepip is not
available.  On Debian/Ubuntu systems, you need to install the python3-venv
package using the following command.

    apt install python3.12-venv

آن را نصب کنید و سپس محیط را با کاربری که مالک کد خواهد بود، ایجاد نمایید.

sudo apt update
sudo apt install -y python3-venv
sudo install -d -o deploy -g deploy -m 755 /srv/myapp
sudo -u deploy python3 -m venv /srv/myapp/.venv
sudo -u deploy /srv/myapp/.venv/bin/pip install -r /srv/myapp/requirements.txt

دقت کنید چه چیزی وجود ندارد: نه source و نه activate. دستور /srv/myapp/.venv/bin/pip به دلیل موقعیت فایل باینری در آن محیط نصب می‌شود، نه به خاطر چیزی که در shell خود export کرده‌اید. پیش از ادامه، این موضوع را تأیید کنید.

/srv/myapp/.venv/bin/python -c 'import sys; print(sys.prefix)'

این دستور /srv/myapp/.venv را چاپ می‌کند. اگر /usr را چاپ کرد، شما در حال اجرای مفسر سیستم هستید و بسته‌های شما به جایی رفته‌اند که قصد آن را نداشتید.

دو ویژگی یک venv تعیین می‌کند که بعداً چه کاری می‌توانید با آن انجام دهید. یک venv قابل جابه‌جایی (relocatable) نیست، زیرا هر اسکریپت در bin/ دارای یک خط shebang مطلق است: head -1 /srv/myapp/.venv/bin/pip فایل #!/srv/myapp/.venv/bin/python را می‌خواند. اگر دایرکتوری والد را تغییر نام دهید، آن اسکریپت‌ها با خطای bad interpreter: No such file or directory مواجه می‌شوند. یک venv همچنین مفسری که آن را ایجاد کرده است را ثابت نگه می‌دارد، که به عنوان خط home در /srv/myapp/.venv/pyvenv.cfg ثبت شده است، و bin/python3 یک symlink به آن فایل باینری است. اگر نسخه سیستم‌عامل را ارتقا دهید به‌طوری که python3.12 حذف شود، symlink هدفی نخواهد داشت و سرویس هنگام شروع با خطای No such file or directory متوقف می‌شود. هر دو مورد راه حل یکسانی دارند: venv را حذف کرده و یک نمونه جدید از روی requirements.txt بسازید. بازسازی تنها چند ثانیه طول می‌کشد. هرگز یک venv را بین ماشین‌های مختلف کپی نکنید.

محل قرارگیری venv و مالکیت آن

آن را در کنار کد در مسیر /srv/myapp/.venv قرار دهید و برای هر برنامه یک venv مجزا در نظر بگیرید. در این صورت، استقرار برنامه شامل یک دایرکتوری واحد خواهد بود، unit مربوط به systemd مسیری ثابت خواهد داشت و دو برنامه هرگز با ارتقای وابستگی‌های مشترک، یکدیگر را دچار اختلال نمی‌کنند. venv را در هیچ مسیری که وب‌سرور شما مستقیماً فایل‌های آن را منتشر می‌کند قرار ندهید، زیرا این محیط حاوی وابستگی‌ها و اغلب تنظیمات شماست.

مالکیت فایل‌ها نیازمند کمی دقت است. اجازه دهید یک کاربر deploy مالک کد و محیط باشد و به حساب کاربری سرویس، فقط دسترسی خواندن و اجرا بدهید.

sudo adduser --system --group --no-create-home myapp
sudo chown -R deploy:myapp /srv/myapp
sudo chmod -R o-rwx /srv/myapp

سرویس اکنون می‌تواند وابستگی‌های خود را فراخوانی کند اما قادر به بازنویسی آن‌ها نیست؛ این یعنی یک باگ اجرای کد در برنامه وب نمی‌تواند بی‌سروصدا یک کتابخانه را روی دیسک جایگزین کند و پس از راه‌اندازی مجدد نیز باقی بماند. همین استدلال برای سایر بخش‌های سیستم در اجرای سرویس‌ها با کاربر دارای حداقل دسترسی پوشش داده شده است.

استفاده از pipx برای ابزارهای خط فرمان

ابزار pipx برنامه‌ها را نصب می‌کند، نه کتابخانه‌ها را. هر ابزار محیط اختصاصی خود را در ~/.local/share/pipx/venvs/<name> دریافت می‌کند و فایل‌های اجرایی آن در ~/.local/bin لینک می‌شوند؛ بنابراین دو ابزاری که به نسخه‌های متفاوتی از یک کتابخانه نیاز دارند، هرگز با هم تداخل پیدا نمی‌کنند.

sudo apt update
sudo apt install -y pipx
pipx ensurepath
pipx install httpie

دستور pipx ensurepath با ویرایش فایل راه‌انداز shell، مسیر ~/.local/bin را به PATH اضافه می‌کند. این دستور نمی‌تواند shell فعلی شما را تغییر دهد، بنابراین خطای http: command not found بلافاصله پس از نصب، معمولاً به این معنی است که شما هنوز از سیستم خارج و دوباره وارد نشده‌اید. فایل ~/.profile پیش‌فرض در Ubuntu، مسیر ~/.local/bin را تنها زمانی اضافه می‌کند که آن دایرکتوری در لحظه ورود وجود داشته باشد؛ به همین دلیل است که این مشکل فقط یک بار در حساب‌های کاربری جدید رخ می‌دهد و دیگر تکرار نمی‌شود.

اگر pipx را به سمت یک کتابخانه هدایت کنید، با پیامی که با عبارت زیر شروع می‌شود، از انجام آن خودداری می‌کند:

No apps associated with package requests or its dependencies.

این پیام ابزار است که به شما می‌گوید از ابزار اشتباهی استفاده کرده‌اید. کتابخانه‌ها باید در venv مربوط به یک برنامه قرار بگیرند.

جزئیاتی که در یک سرور اهمیت دارد، محل نصب است. یک دستور pipx install ساده، همه چیز را در دایرکتوری home یک کاربر قرار می‌دهد. یک unit در systemd که با کاربر myapp اجرا می‌شود، یک cron job که با کاربر root اجرا می‌شود و همچنین sudo، نمی‌توانند آن را ببینند؛ زیرا secure_path در /etc/sudoers، مقدار PATH را با یک لیست ثابت جایگزین می‌کند. برای ابزاری که کل سیستم باید به آن دسترسی داشته باشد، آن را به‌صورت سراسری (globally) نصب کنید.

sudo pipx install --global ansible
sudo pipx ensurepath --global

فلگ --global محیط‌ها را در /opt/pipx قرار داده و فایل‌های اجرایی را در /usr/local/bin لینک می‌کند که در PATH پیش‌فرض و داخل secure_path قرار دارد. ابتدا نسخه خود را با pipx --version بررسی کنید، زیرا Ubuntu 24.04 نسخه 1.4.3 از pipx را ارائه می‌دهد که قدیمی‌تر از --global است و نسخه‌های قدیمی‌تر pipx با unrecognized arguments: --global پاسخ می‌دهند. در آن نسخه، دو دایرکتوری مستندشده را خودتان تنظیم کنید:

sudo env PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ansible
command -v ansible

دستور command -v ansible باید /usr/local/bin/ansible را چاپ کند. اگر مسیری در زیرشاخه /home را چاپ کرد، ابزار در حساب یک کاربر خاص نصب شده است و هیچ سرویسی آن را پیدا نخواهد کرد.

uv زمانی که به یک lockfile نیاز دارید

uv یک فایل اجرایی واحد از Astral است که وظایف pip، venv و pip-tools را پوشش می‌دهد و همچنین می‌تواند مفسرهای پایتون را دانلود کند. سرعت آن به‌قدری بالاست که تفاوتش در یک VPS کوچک محسوس است و یک lockfile واقعی تولید می‌کند.

نصب‌کننده رسمی، uv و uvx را در ~/.local/bin قرار می‌دهد:

curl -LsSf https://astral.sh/uv/install.sh | sh
uv --version

ارسال مستقیم یک اسکریپت به shell در سرور، نیازمند کمی دقت است. نسخه موجود در URL را ثابت (pin) کنید و پیش از اجرا، محتوای فایل را بخوانید:

curl -LsSf https://astral.sh/uv/0.12.3/install.sh -o uv-install.sh
less uv-install.sh
sh uv-install.sh

اگر pipx از قبل نصب باشد، pipx install uv نیز کار می‌کند. uv یک فایل اجرایی مستقل است که هیچ وابستگی به پایتون ندارد، بنابراین کپی کردن آن در /usr/local/bin روشی معتبر برای در دسترس قرار دادن آن برای تمام کاربران سیستم است.

برای پروژه‌ای که دارای pyproject.toml است، گردش کار شامل چهار دستور است که فقط آخرین مورد آن روی سرور اجرا می‌شود.

uv init myapp
uv add flask gunicorn
uv lock
uv sync --frozen --no-dev

uv lock فایل uv.lock را می‌نویسد؛ یک lockfile چندپلتفرمی که نسخه‌های دقیق و حل‌شده (resolved) را نگه می‌دارد و شما آن را همراه با کد خود commit می‌کنید. uv sync پوشه .venv را در ریشه پروژه مطابق با آن می‌سازد. روی سرور، --frozen پرچم مهمی است: مستندات آن را به عنوان استفاده از نسخه‌های موجود در lockfile به عنوان منبع حقیقت تعریف می‌کنند، به‌جای اینکه بررسی کند آیا lockfile به‌روز است یا خیر؛ این همان رفتاری است که در deployment به آن نیاز داریم. --no-dev گروه وابستگی‌های توسعه (development) را نادیده می‌گیرد.

یک پروژه requirements.txt موجود نیازی به تبدیل ندارد، زیرا uv زبان pip را می‌فهمد:

uv venv /srv/myapp/.venv
uv pip install --python /srv/myapp/.venv/bin/python -r /srv/myapp/requirements.txt

خروجی، یک virtual environment معمولی است. .venv/bin/python دقیقاً همان‌طور رفتار می‌کند که اگر python3 -m venv آن را ساخته بود رفتار می‌کرد، بنابراین هیچ‌چیز در ادامه این راهنما تغییر نمی‌کند.

یک تنظیم پیش‌فرض در uv وجود دارد که پیش از استفاده روی سرور باید از آن آگاه باشید. تنظیم python-preference به‌صورت پیش‌فرض روی managed است که طبق مستندات، اولویت را به "مفسرهایی که توسط uv دانلود و نصب می‌شوند" می‌دهد، نه مفسرهای موجود در سیستم. بنابراین uv venv --python 3.13 روی سیستمی که فقط پایتون 3.12 دارد، بی‌سروصدا نسخه 3.13 را در ~/.local/share/uv/python دانلود می‌کند به‌جای اینکه با خطا مواجه شود. این رفتار روی لپ‌تاپ راحت است اما روی سرور غافلگیرکننده است، زیرا سرویس شما اکنون به مفسری وابسته است که در یک دایرکتوری خانگی (home directory) قرار دارد و apt upgrade هرگز آن را وصله (patch) نخواهد کرد. اگر مفسر توزیع سیستم‌عامل را می‌خواهید، python-preference را در uv.toml روی only-system تنظیم کنید. اگر می‌خواهید محیط مجازی در جایی غیر از ریشه پروژه باشد، UV_PROJECT_ENVIRONMENT دایرکتوری مورد استفاده برای virtual environment پروژه را مشخص می‌کند.

اشاره به مفسر venv در systemd، نه به activate

اینجاست که اکثر استقرار‌های پایتون با شکست مواجه می‌شوند و علت آن، درک نادرست از عملکرد activate است.

bin/activate یک اسکریپت شل است. این اسکریپت دایرکتوری bin مربوط به venv را به ابتدای PATH اضافه می‌کند، VIRTUAL_ENV را تنظیم کرده، مقادیر قبلی را ذخیره می‌کند تا deactivate بتواند آن‌ها را بازیابی کند و پرامپت شما را تغییر می‌دهد. این اسکریپت حاوی هیچ چیزی نیست که خود مفسر آن را بخواند. فعال‌سازی (Activation) صرفاً یک ابزار رفاهی برای انسانی است که python را در پرامپت تایپ می‌کند.

آنچه واقعاً محیط را انتخاب می‌کند، فایلی از مفسر است که اجرا می‌کنید. وقتی /srv/myapp/.venv/bin/python شروع به کار می‌کند، ماژول site پایتون به دنبال فایلی به نام pyvenv.cfg در دایرکتوری حاوی فایل اجرایی و یک سطح بالاتر از آن می‌گردد. با یافتن /srv/myapp/.venv/pyvenv.cfg، مقدار sys.prefix روی venv تنظیم می‌شود که باعث قرارگیری site-packages آن venv در sys.path می‌گردد. این کل مکانیزم است. به هیچ متغیر محیطی و هیچ شلی نیاز ندارد.

بنابراین این unit هرگز شروع نمی‌شود:

[Service]
ExecStart=source /srv/myapp/.venv/bin/activate && gunicorn app:app
myapp.service: Failed to locate executable source: No such file or directory
myapp.service: Failed at step EXEC spawning source: No such file or directory
myapp.service: Main process exited, code=exited, status=203/EXEC

ExecStart یک خط فرمان شل نیست. systemd یک برنامه را مستقیماً اجرا می‌کند، بنابراین هیچ دستور داخلی source وجود ندارد، && به عنوان یک آرگومان تحت‌اللفظی ارسال می‌شود و هیچ چیزی بسط (expand) نمی‌یابد.

و این unit شروع می‌شود، اما سپس متوقف می‌گردد:

[Service]
ExecStart=/usr/bin/python3 /srv/myapp/app.py
ModuleNotFoundError: No module named 'flask'

/usr/bin/python3 مفسر سیستم است و sys.path آن هرگز شامل venv شما نبوده است. همان دستور در نشست SSH شما فقط به این دلیل کار می‌کند که شما venv را در آنجا فعال کرده بودید، بنابراین شل، python3 را از طریق PATH به .venv/bin/python3 تبدیل کرده بود.

پیچیدن دستور در /bin/bash -c 'source ... && gunicorn ...' کار می‌کند. اما این کار یک شل را بدون هیچ دستاوردی بین systemd و پردازش شما قرار می‌دهد، در حالی که یک مسیر مطلق (absolute path) مشکل را حل می‌کند:

[Unit]
Description=myapp web service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp
Environment=PYTHONUNBUFFERED=1
Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/srv/myapp/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 app:app
Restart=on-failure
RestartSec=5
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp
journalctl -u myapp -n 50 --no-pager

systemctl status myapp باید active (running) را با Main PID که همان پردازش gunicorn شماست، گزارش دهد. در غیر این صورت، journal را بخوانید.

خط Environment=PATH= برای ExecStart که از قبل مسیر کامل را دارد، نیست. این خط برای پردازش‌هایی است که برنامه شما شروع می‌کند. یک سرویس، یک PATH پیش‌فرض کوتاه از systemd به ارث می‌برد، بنابراین کد پایتونی که subprocess.run(["ffmpeg", ...]) را فراخوانی می‌کند، یا یک دستور مدیریتی که به یک اسکریپت کنسول از venv شل می‌زند، آنچه نیاز دارد را پیدا نخواهد کرد. قرار دادن دایرکتوری bin مربوط به venv در ابتدای مسیر، تنها بخشی از activate است که یک سرویس واقعاً از آن استفاده می‌کند. با systemctl show -p Environment myapp بررسی کنید که unit واقعاً چه چیزی دریافت کرده است.

همین قانون برای کارهای زمان‌بندی شده نیز صدق می‌کند. cron کارها را با PATH برابر با /usr/bin:/bin اجرا می‌کند، بنابراین یک خط در crontab که python3 /srv/myapp/cleanup.py را می‌خواند، مفسر سیستم را اجرا کرده و در ساعت سه صبح با ModuleNotFoundError شکست می‌خورد و خطا به یک mail spool محلی می‌رود که هیچ‌کس آن را نمی‌خواند. مسیر مطلق venv را در آنجا نیز بنویسید. برای دریافت آن خروجی در journal و داشتن سوابق آخرین اجرا، یک جفت سرویس و تایمر systemd از همان خط ExecStart استفاده می‌کند.

آیا Docker این تصمیم را جایگزین می‌کند؟

هر کانتینر فایل‌سیستم اختصاصی خود را دارد، بنابراین صورت مسئله تغییر می‌کند اما از بین نمی‌رود. در یک ایمیج رسمی مانند python:3.12-slim، پایتون درون /usr/local ساخته شده و هیچ نشانگر EXTERNALLY-MANAGED ندارد؛ بنابراین pip install به عنوان root روشی است که برای افزودن پکیج‌ها در نظر گرفته شده و استفاده از venv در اینجا مزیت چندانی ندارد. در عوض، اگر FROM ubuntu:24.04 را بسازید، دوباره با externally-managed-environment درون ایمیج مواجه می‌شوید؛ به همان دلیلی که روی هاست وجود داشت: این مفسر توزیع است که فایل نشانگر همان توزیع را به همراه دارد.

بسیاری از ایمیج‌ها همچنان از venv استفاده می‌کنند، زیرا ساخت چندمرحله‌ای (multi-stage build) را ساده می‌کند. مرحله builder پکیج‌ها را در /opt/venv نصب می‌کند و مرحله runtime فقط همان دایرکتوری را کپی کرده و کامپایلرها را کنار می‌گذارد. مشکل فعال‌سازی (activate) همراه با آن منتقل می‌شود. یک خط RUN source /opt/venv/bin/activate فقط بر شل همان لایه از ساخت تأثیر می‌گذارد، بنابراین در زمان اجرا، کانتینر با مفسر سیستم شروع به کار کرده و خطای ModuleNotFoundError را صادر می‌کند. متغیر ENV PATH="/opt/venv/bin:$PATH" را تنظیم کنید یا به CMD مسیر مطلق /opt/venv/bin/gunicorn را بدهید. این همان باگ موجود در systemd است، فقط در فایلی متفاوت.

بنابراین، کانتینر مسئله مفسر را جایگزین می‌کند، زیرا ایمیج، مفسر و تمام محتویات زیرمجموعه آن را ثابت (pin) می‌کند. اما مسئله ثابت‌سازی نسخه‌ها را جایگزین نمی‌کند. ایمیجی که از یک requirements.txt بدون نسخه ثابت ساخته شده باشد، ماه آینده نسخه‌های متفاوتی را resolve می‌کند؛ این یعنی تگ ایمیج قابل بازتولید است، اما فرآیند ساختی که آن را تولید کرده، خیر. یک فایل قفل (lockfile) مانند uv.lock یا یک فایل requirements که تمام نسخه‌ها در آن قید شده باشد، همان چیزی است که این شکاف را پر می‌کند، چه در کانتینر و چه خارج از آن. و هنگامی که یک اپلیکیشن روی یک VPS تحت systemd اجرا می‌شود، کانتینر عمدتاً همین تصمیم را به یک Dockerfile منتقل می‌کند، چرا که systemd از قبل فرآیند شکست‌خورده را ری‌استارت کرده و خروجی آن را در journal ثبت می‌کند. اجرای Docker روی یک VPS زمانی ارزش خود را نشان می‌دهد که بخواهید خودِ ایمیج ساخته‌شده، همان چیزی باشد که مستقر (deploy) می‌کنید.

FAQ

آیا می‌توانم از pip install با فلگ --break-system-packages استفاده کنم؟

خیر، نه روی سروری که باید همیشه در دسترس باشد. این فلگ دقیقاً همان کاری را انجام می‌دهد که نامش می‌گوید: محافظت را حذف می‌کند و pip فایل‌ها را در /usr/local/lib/python3.12/dist-packages می‌نویسد که در مسیر sys.path پیش از دایرکتوری apt قرار دارد. در نتیجه، نسخهٔ شما جایگزین نسخهٔ توزیع برای تمام اسکریپت‌های سیستمی می‌شود که تحت /usr/bin/python3 اجرا می‌شوند؛ در حالی که apt همچنان فکر می‌کند نسخهٔ خودش نصب است. بنابراین تا زمانی که مشکلی رخ ندهد، هیچ‌کس متوجه این تداخل نخواهد شد. در داخل یک image کانتینر که هر بار از صفر بازسازی می‌شود، آسیب فقط به همان image محدود است و استفاده از آن توجیه‌پذیر است. اما روی ماشینی که مدیریت می‌کنید، یک venv بسازید. این کار تنها با یک دستور انجام می‌شود.

محیط مجازی (virtual environment) باید در کجای سرور قرار بگیرد؟

در داخل دایرکتوری خودِ برنامه، با نام /srv/myapp/.venv، که مالکیت آن با کاربر deploy باشد و حساب کاربری سرویس فقط دسترسی خواندن و اجرا داشته باشد. برای هر برنامه یک venv جداگانه نگه دارید، زیرا استفاده از یک محیط مشترک باعث می‌شود ارتقای برنامهٔ اول، برنامهٔ دوم را دچار اختلال کند. پس از ساخت venv، آن را جابه‌جا یا کپی نکنید: تمام اسکریپت‌های موجود در دایرکتوری bin/ آن، مسیر مطلق را در خط shebang خود ثبت کرده‌اند؛ بنابراین venv جابه‌جاشده با خطای bad interpreter: No such file or directory از کار می‌افتد. در عوض، آن را حذف کرده و از روی requirements.txt دوباره بسازید.

چرا سرویس systemd من با خطای ModuleNotFoundError شکست می‌خورد؟

زیرا unit در حال اجرای مفسری است که متعلق به venv نیست. دستور systemctl cat myapp را اجرا کنید و ExecStart را بخوانید. این فایل باید به /srv/myapp/.venv/bin/python یا یک اسکریپت کنسول از همان دایرکتوری bin/ با مسیر مطلق اشاره کند. اجرای دستور source برای activate در یک فایل unit کارساز نیست، زیرا ExecStart یک shell نیست و systemd خطای Failed to locate executable source را با status=203/EXEC گزارش می‌دهد. گزینهٔ Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin را اضافه کنید تا هر زیرفرآیندی که کد شما شروع می‌کند نیز ابزارهای venv را پیدا کند.

آیا باید به‌جای venv و pip از uv استفاده کنم؟

زمانی از uv استفاده کنید که به یک فایل lock نیاز دارید، زمان نصب آن‌قدر طولانی است که شما را آزار می‌دهد، یا به نسخه‌ای از Python نیاز دارید که توزیع شما ارائه نمی‌دهد. uv یک venv معمولی می‌سازد، بنابراین unit مربوط به systemd و ساختار فایل‌ها تغییری نمی‌کند و uv sync --frozen دقیقاً همان چیزی را نصب می‌کند که در فایل lock ثبت شده است. اگر یک برنامهٔ واحد را از git با یک requirements.txt ثابت مستقر می‌کنید و نصب آن در چند ثانیه تمام می‌شود، python3 -m venv کافی است و یک باینری کمتر برای به‌روزرسانی روی سرور خواهید داشت.

#python#venv#pipx#uv#deployment