SSD Nodes Learn 🎉 VPS เริ่ม $5.50/เดือน
คู่มือ Matt Connorโดย Matt Connor · อัปเดตเมื่อ 2026-08-13

เลือกใช้ Python venv, pipx หรือ uv บนเซิร์ฟเวอร์อย่างไรดี

แก้ไขปัญหา pip install ล้มเหลวด้วยข้อผิดพลาด externally-managed-environment บน Ubuntu 24.04 เรียนรู้วิธีเลือกเครื่องมือจัดการแพ็กเกจที่เหมาะสมและตั้งค่าผ่าน systemd

เหตุใด pip install จึงล้มเหลวบนเซิร์ฟเวอร์ Ubuntu ที่ติดตั้งใหม่

การเลือกระหว่าง Python venv, pipx และ uv บนเซิร์ฟเวอร์ขึ้นอยู่กับคำถามเดียวคือ คุณกำลังติดตั้งอะไรอยู่? dependency ของแอปพลิเคชันควรอยู่ใน virtual environment ภายในไดเรกทอรีของแอปพลิเคชันนั้นๆ ส่วนเครื่องมือบรรทัดคำสั่ง (command-line tools) ที่คุณต้องการเรียกใช้ด้วยชื่อคำสั่งควรติดตั้งผ่าน 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 (Python enhancement proposal 668, "สภาพแวดล้อมที่จัดการจากภายนอก") โดย Debian และ Ubuntu จะวางไฟล์เครื่องหมายไว้ข้างตัวแปลภาษา (interpreter) ที่ /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 รวมถึงเครื่องมือของตัว distribution เองด้วย เช่น cloud-init ที่ import requests, jinja2 และ PyYAML จากตัวแปลภาษานั้น หากคุณอัปเกรดไลบรารีเหล่านี้ด้วย pip แล้วได้เวอร์ชันที่ไม่เข้ากัน สิ่งที่คุณไม่เคยแตะต้องอาจล้มเหลวในการบูตครั้งถัดไปพร้อมกับ traceback ที่ระบุชื่อแพ็กเกจที่คุณไม่ทราบมาก่อนว่าอยู่ในห่วงโซ่การทำงาน ในขณะที่ apt ยังคงบันทึกว่าเวอร์ชันของตนเองถูกติดตั้งอยู่ จึงไม่มีการแจ้งเตือนใดๆ และการแก้ไขทำได้เพียง sudo apt reinstall python3-requests เท่านั้น

กฎที่ตามมานั้นสั้นและง่าย Python ของระบบเป็นของตัว distribution ห้ามติดตั้งสิ่งใดลงไป ห้ามอัปเกรดไลบรารีของระบบด้วย pip และห้ามลบไฟล์ EXTERNALLY-MANAGED เพื่อให้ข้อความแจ้งเตือนหายไป หน้าที่เดียวที่คุณควรให้ /usr/bin/python3 ทำคือการสร้าง virtual environments เท่านั้น

เกณฑ์การตัดสินใจเลือกใช้ venv, pipx และ uv

ให้เลือกเครื่องมือตามประเภทของสิ่งที่คุณกำลังติดตั้ง ไม่ใช่เลือกตามเครื่องมือที่คุณเพิ่งอ่านเจอมาล่าสุด

  • แอปพลิเคชันที่คุณนำไปใช้งานและรันเป็น service เช่น โปรเจกต์ Django หรือ Flask: ให้ใช้ virtual environment (venv) หนึ่งชุดภายในไดเรกทอรีของแอปพลิเคชันนั้น
  • เครื่องมือบรรทัดคำสั่ง (command-line tool) ที่คุณต้องการเรียกใช้บน PATH ของคุณ เช่น ansible หรือ httpie: ให้ใช้ pipx ซึ่งจะสร้างสภาพแวดล้อมส่วนตัวให้แต่ละเครื่องมือและสร้างลิงก์ไว้บน PATH
  • โปรเจกต์ที่ต้องการ lockfile, การติดตั้งที่รวดเร็วขึ้น หรือเวอร์ชัน Python ที่ไม่มีใน distribution: ให้ใช้ uv ซึ่งจะสร้าง venv ปกติควบคู่ไปกับไฟล์ uv.lock
  • ไลบรารีที่เครื่องมือของระบบจำเป็นต้องใช้ (ไม่ใช่ไลบรารีสำหรับโค้ดของคุณ): ให้ใช้ sudo apt install python3-<name> ซึ่งเป็นวิธีเดียวที่รองรับในการเพิ่มสิ่งใดๆ ลงใน system interpreter

pipx และ uv tool install ทำหน้าที่เดียวกัน ดังนั้นเครื่องที่มี uv อยู่แล้วจึงไม่จำเป็นต้องติดตั้ง pipx เพิ่มเติม การเลือกใช้ web framework ไม่ส่งผลต่อเรื่องนี้ Django และ Flask บน VPS มีความแตกต่างกันที่สิ่งที่อยู่ใน requirements.txt ไม่ใช่ที่วิธีการสร้างสภาพแวดล้อมโดยรอบ ทุกขั้นตอนด้านล่างนี้ใช้ Ubuntu 24.04 และ Python 3.12 ดังนั้นหากเวอร์ชันของคุณแตกต่างออกไป ให้ปรับแก้เวอร์ชันภายใน path ให้ถูกต้อง

สร้าง venv สำหรับแต่ละแอปพลิเคชัน

Ubuntu แยกโมดูล venv ออกจากแพ็กเกจ Python หลัก ดังนั้นบนอิมเมจแบบ minimal การพยายามรันครั้งแรกจะล้มเหลวพร้อมข้อความระบุสิ่งที่ขาดหายไปอย่างชัดเจน

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 จะติดตั้งลงในสภาพแวดล้อมนั้นโดยพิจารณาจากตำแหน่งของ binary ไม่ใช่เพราะสิ่งที่คุณ export เข้าสู่ shell ให้ตรวจสอบยืนยันก่อนดำเนินการต่อ

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

คำสั่งนั้นจะแสดงผลเป็น /srv/myapp/.venv หากแสดงผลเป็น /usr แสดงว่าคุณกำลังรันตัวแปลภาษาของระบบ (system interpreter) และแพ็กเกจของคุณถูกติดตั้งไปยังตำแหน่งที่ไม่ต้องการ

คุณสมบัติสองประการของ venv เป็นตัวกำหนดสิ่งที่คุณสามารถทำได้หลังจากนั้น venv ไม่สามารถย้ายที่ได้ (not 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 ไปยัง binary นั้น หากคุณอัปเกรด release จน python3.12 หายไป symlink จะไม่มีเป้าหมาย และ service จะหยุดทำงานเมื่อเริ่มระบบด้วย No such file or directory ทั้งสองกรณีมีวิธีแก้ไขแบบเดียวกันคือ ลบ venv ทิ้งแล้วสร้างใหม่จาก requirements.txt การสร้างใหม่ใช้เวลาเพียงไม่กี่วินาที ห้ามคัดลอก venv ระหว่างเครื่องโดยเด็ดขาด

ตำแหน่งของ venv และผู้ถือสิทธิ์

ให้วาง venv ไว้ข้างกับซอร์สโค้ดที่ /srv/myapp/.venv และใช้ venv แยกกันสำหรับแต่ละแอปพลิเคชัน วิธีนี้จะทำให้การ deployment เป็นไปในรูปแบบไดเรกทอรีเดียว systemd unit จะได้ใช้ path ที่ไม่เปลี่ยนแปลง และแอปพลิเคชันสองตัวจะไม่ส่งผลกระทบต่อกันหากมีการอัปเกรด dependency ร่วมกัน ห้ามวาง venv ไว้ในตำแหน่งที่เว็บเซิร์ฟเวอร์เผยแพร่ไฟล์โดยตรง เนื่องจาก venv เก็บ dependency และมักจะมีไฟล์ตั้งค่าของคุณอยู่ด้วย

เรื่องความเป็นเจ้าของควรให้ความสำคัญสักครู่ ให้ผู้ใช้ deploy เป็นเจ้าของโค้ดและสภาพแวดล้อม และกำหนดสิทธิ์ให้บัญชีผู้ใช้ของ service มีสิทธิ์อ่านและเรียกใช้งาน (read and execute) เท่านั้น

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

ขณะนี้ service สามารถเรียกใช้ dependency ได้แต่ไม่สามารถแก้ไขไฟล์เหล่านั้นได้ ซึ่งหมายความว่าหากเกิดข้อผิดพลาดประเภท code execution ในเว็บแอปพลิเคชัน ผู้โจมตีจะไม่สามารถแอบเปลี่ยนไลบรารีในดิสก์เพื่อให้คงอยู่ถาวรหลังการรีสตาร์ทได้ หลักการเดียวกันนี้ที่นำไปใช้กับส่วนอื่นๆ ของเครื่องถูกอธิบายไว้ใน การรัน service ด้วยสิทธิ์ผู้ใช้ที่จำกัดที่สุด

pipx สำหรับเครื่องมือบรรทัดคำสั่ง

pipx ใช้สำหรับติดตั้งแอปพลิเคชัน ไม่ใช่ไลบรารี เครื่องมือแต่ละตัวจะมีสภาพแวดล้อมแยกเป็นของตัวเองภายใต้ ~/.local/share/pipx/venvs/<name> และไฟล์สั่งการ (executable) ของเครื่องมือนั้นจะถูกเชื่อมโยง (link) เข้าไปใน ~/.local/bin ดังนั้นเครื่องมือสองตัวที่ต้องการไลบรารีเวอร์ชันเดียวกันแต่ต่างกันจะไม่เกิดปัญหาทับซ้อนกัน

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

pipx ensurepath จะเพิ่ม ~/.local/bin เข้าไปใน PATH โดยการแก้ไขไฟล์เริ่มต้นของเชลล์คุณ มันไม่สามารถเปลี่ยนแปลงเชลล์ที่คุณกำลังใช้งานอยู่ได้ ดังนั้น http: command not found ที่เกิดขึ้นทันทีหลังการติดตั้งมักหมายความว่าคุณยังไม่ได้ออกจากระบบและล็อกอินเข้ามาใหม่ ค่าเริ่มต้นของ ~/.profile ใน Ubuntu จะเพิ่ม ~/.local/bin ก็ต่อเมื่อไดเรกทอรีนั้นมีอยู่แล้วในขณะล็อกอิน นี่คือสาเหตุที่ปัญหานี้มักเกิดขึ้นเพียงครั้งเดียวในบัญชีผู้ใช้ใหม่และจะไม่เกิดขึ้นอีก

หากคุณระบุไลบรารีให้ pipx มันจะปฏิเสธพร้อมข้อความที่ขึ้นต้นว่า:

No apps associated with package requests or its dependencies.

นั่นคือเครื่องมือที่กำลังบอกคุณว่าคุณใช้เครื่องมือผิดประเภท ไลบรารีควรอยู่ใน venv ของแอปพลิเคชัน

รายละเอียดที่สำคัญบนเซิร์ฟเวอร์คือตำแหน่งที่ตั้ง การใช้ pipx install แบบปกติจะเก็บทุกอย่างไว้ภายใต้โฮมไดเรกทอรีของผู้ใช้คนเดียว หน่วยของ systemd ที่รันในฐานะ myapp จะมองไม่เห็นมัน, งาน cron ของ root ก็มองไม่เห็นมัน และ sudo ก็จะไม่พบมันเช่นกัน เพราะ secure_path ใน /etc/sudoers จะแทนที่ PATH ด้วยรายการที่กำหนดไว้ตายตัว สำหรับเครื่องมือที่ทั้งเครื่องควรใช้งานได้ ให้ติดตั้งแบบ global

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

แฟล็ก --global จะวางสภาพแวดล้อมไว้ใน /opt/pipx และเชื่อมโยงไฟล์สั่งการไว้ใน /usr/local/bin ซึ่งอยู่ใน PATH ค่าเริ่มต้นและอยู่ภายใน secure_path ให้ตรวจสอบเวอร์ชันของคุณก่อนด้วย pipx --version เนื่องจาก Ubuntu 24.04 มีแพ็กเกจ pipx 1.4.3 ซึ่งเก่ากว่า --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 เป็นไฟล์ binary เดี่ยวจาก Astral ที่รวมความสามารถของ pip, venv และ pip-tools เข้าด้วยกัน อีกทั้งยังสามารถดาวน์โหลด Python interpreter ได้ในตัว uv มีความเร็วสูงจนเห็นความแตกต่างได้ชัดเจนบน VPS ขนาดเล็ก และสามารถสร้าง lockfile ที่ใช้งานได้จริง

ตัวติดตั้งอย่างเป็นทางการจะวาง uv และ uvx ไว้ที่ ~/.local/bin:

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

การส่งสคริปต์ผ่าน pipe เข้าสู่ shell บนเซิร์ฟเวอร์ควรใช้ความระมัดระวัง คุณควรระบุเวอร์ชันใน URL ให้ชัดเจนและอ่านเนื้อหาในไฟล์ก่อนเรียกใช้งาน:

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

pipx install uv ก็สามารถใช้งานได้เช่นกันหากติดตั้ง pipx ไว้แล้ว เนื่องจาก uv เป็น binary แบบเบ็ดเสร็จในตัวเดียวและไม่มี dependency เป็น Python การคัดลอกไฟล์ไปไว้ที่ /usr/local/bin จึงเป็นวิธีที่เหมาะสมในการทำให้ผู้ใช้ทุกคนบนเครื่องสามารถเรียกใช้งานได้

สำหรับโปรเจกต์ที่มี pyproject.toml เวิร์กโฟลว์จะมีทั้งหมด 4 คำสั่ง โดยคำสั่งสุดท้ายเท่านั้นที่จะถูกเรียกใช้บนเซิร์ฟเวอร์:

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

uv lock จะเขียนไฟล์ uv.lock ซึ่งเป็น lockfile แบบข้ามแพลตฟอร์มที่เก็บเวอร์ชันที่แก้ไขแล้วไว้อย่างแม่นยำ คุณควร commit ไฟล์นี้ไว้พร้อมกับโค้ดของคุณ uv sync จะสร้าง .venv ไว้ใน root ของโปรเจกต์ให้ตรงกับ lockfile บนเซิร์ฟเวอร์ แฟล็ก --frozen คือส่วนสำคัญ โดยเอกสารระบุว่าเป็นการใช้เวอร์ชันใน lockfile เป็นแหล่งข้อมูลหลักแทนการตรวจสอบว่า lockfile เป็นเวอร์ชันล่าสุดหรือไม่ ซึ่งเป็นพฤติกรรมที่เหมาะสมสำหรับการ deploy ส่วน --no-dev จะเป็นการละเว้นกลุ่ม dependency สำหรับการพัฒนา

โปรเจกต์ 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" แทนการใช้ interpreter ที่มีอยู่แล้วในระบบ ดังนั้น uv venv --python 3.13 บนเครื่องที่มาพร้อมกับ Python 3.12 จะทำการดาวน์โหลด 3.13 มาไว้ที่ ~/.local/share/uv/python โดยอัตโนมัติแทนที่จะแจ้งเตือนว่าไม่สำเร็จ สิ่งนี้อาจสะดวกสำหรับแล็ปท็อปแต่เป็นเรื่องที่คาดไม่ถึงบนเซิร์ฟเวอร์ เพราะบริการของคุณจะขึ้นอยู่กับ interpreter ที่อยู่ใน home directory ซึ่ง apt upgrade จะไม่ทำการ patch ให้ หากคุณต้องการใช้ interpreter ของ distribution ให้ตั้งค่า python-preference เป็น only-system ใน uv.toml และหากคุณต้องการให้ virtual environment อยู่ในตำแหน่งอื่นที่ไม่ใช่ root ของโปรเจกต์ สามารถใช้ UV_PROJECT_ENVIRONMENT เพื่อระบุไดเรกทอรีที่ต้องการได้

กำหนดให้ systemd ใช้ตัวแปลภาษาใน venv โดยตรง แทนการใช้ activate

นี่คือจุดที่การติดตั้ง Python ส่วนใหญ่เกิดปัญหา และสาเหตุมาจากความเข้าใจผิดว่า activate ทำหน้าที่อะไร

bin/activate เป็นเพียงเชลล์สคริปต์ มันจะเพิ่มไดเรกทอรี bin ของ venv เข้าไปที่ต้น PATH, ตั้งค่า VIRTUAL_ENV, บันทึกค่าเดิมไว้เพื่อให้ deactivate เรียกคืนได้ และเปลี่ยนข้อความแจ้งเตือน (prompt) ของคุณ มันไม่มีสิ่งใดที่ตัวแปลภาษาอ่านโดยตรง การ activate เป็นเพียงความสะดวกสำหรับมนุษย์ที่พิมพ์ python ที่บรรทัดคำสั่งเท่านั้น

สิ่งที่กำหนดสภาพแวดล้อมจริงๆ คือไฟล์ตัวแปลภาษาที่คุณเรียกใช้งาน เมื่อ /srv/myapp/.venv/bin/python เริ่มทำงาน โมดูล site ของ Python จะมองหาไฟล์ 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 ในตัว, && จะถูกส่งไปเป็นอาร์กิวเมนต์ตามตัวอักษร และไม่มีการขยายค่าใดๆ เกิดขึ้น

และ unit นี้เริ่มทำงานแล้วก็ดับไป:

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

/usr/bin/python3 คือตัวแปลภาษาของระบบ และ sys.path ของมันไม่เคยมี venv ของคุณอยู่ คำสั่งเดียวกันนี้ทำงานในเซสชัน SSH ของคุณได้เพียงเพราะคุณได้ activate 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 ของคุณ หากเป็นอย่างอื่น ให้ตรวจสอบ log ใน journal

บรรทัด Environment=PATH= ไม่ได้มีไว้สำหรับ ExecStart ซึ่งมี path เต็มอยู่แล้ว แต่มีไว้สำหรับกระบวนการที่แอปพลิเคชันของคุณเรียกใช้งาน บริการหนึ่งๆ จะได้รับ PATH ค่าเริ่มต้นที่สั้นมากจาก systemd ดังนั้นโค้ด Python ที่เรียก subprocess.run(["ffmpeg", ...]) หรือคำสั่งจัดการที่เรียกสคริปต์คอนโซลจาก venv จะไม่พบสิ่งที่ต้องการ การนำไดเรกทอรี bin ของ venv มาไว้เป็นอันดับแรกคือส่วนเดียวของ activate ที่บริการใช้งานจริง ตรวจสอบสิ่งที่ unit ได้รับจริงๆ ด้วย systemctl show -p Environment myapp

กฎเดียวกันนี้ครอบคลุมถึงงานที่ตั้งเวลาไว้ cron จะรันงานด้วย PATH เป็น /usr/bin:/bin ดังนั้นบรรทัดใน crontab ที่เขียนว่า python3 /srv/myapp/cleanup.py จะรันตัวแปลภาษาของระบบและล้มเหลวด้วย ModuleNotFoundError ในเวลาตีสาม และข้อผิดพลาดจะถูกส่งไปยัง local mail spool ที่ไม่มีใครอ่าน ให้เขียน absolute path ของ venv ลงไปในนั้นด้วย เพื่อให้ได้ผลลัพธ์ใน journal และมีบันทึกการรันครั้งล่าสุด ให้ใช้ คู่ของ systemd service และ timer ซึ่งใช้บรรทัด ExecStart เดียวกันนี้

Docker เข้ามาแทนที่การตัดสินใจนี้หรือไม่?

Container มีระบบไฟล์เป็นของตัวเอง ดังนั้นคำถามนี้จึงเปลี่ยนรูปแบบไปแทนที่จะหายไป ในอิมเมจทางการอย่าง python:3.12-slim ตัว Python ถูกรวมไว้ใน /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 ส่งผลต่อ shell ของเลเยอร์การ build นั้นเท่านั้น ดังนั้นในขณะ runtime คอนเทนเนอร์จะเริ่มทำงานบนอินเทอร์พรีเตอร์ของระบบและแจ้งเตือน ModuleNotFoundError คุณต้องตั้งค่า ENV PATH="/opt/venv/bin:$PATH" หรือระบุพาธแบบสัมบูรณ์ /opt/venv/bin/gunicorn ให้กับ CMD มันคือบั๊กเดียวกับที่พบใน systemd เพียงแต่อยู่ในไฟล์ที่ต่างออกไป

ดังนั้น คอนเทนเนอร์เข้ามาแทนที่คำถามเรื่องอินเทอร์พรีเตอร์ เพราะอิมเมจจะล็อกเวอร์ชันของอินเทอร์พรีเตอร์และทุกสิ่งที่อยู่ภายใต้มันไว้ แต่มันไม่ได้เข้ามาแทนที่คำถามเรื่องการล็อกเวอร์ชัน (pinning) อิมเมจที่สร้างจาก requirements.txt ที่ไม่ได้ล็อกเวอร์ชันไว้จะทำให้ได้เวอร์ชันที่ต่างออกไปในเดือนถัดมา ซึ่งหมายความว่าแท็กของอิมเมจนั้นทำซ้ำได้ (reproducible) แต่กระบวนการ build ที่สร้างอิมเมจนั้นกลับทำซ้ำไม่ได้ ไฟล์ล็อกอย่าง uv.lock หรือไฟล์ requirements ที่ล็อกเวอร์ชันไว้อย่างครบถ้วน คือสิ่งที่ช่วยปิดช่องว่างนี้ ไม่ว่าจะใช้คอนเทนเนอร์หรือไม่ก็ตาม และเมื่อแอปพลิเคชันหนึ่งรันบน VPS หนึ่งเครื่องภายใต้ systemd คอนเทนเนอร์ส่วนใหญ่จะย้ายการตัดสินใจนี้ไปไว้ใน Dockerfile เนื่องจาก systemd สามารถรีสตาร์ทกระบวนการที่ล้มเหลวและบันทึกเอาต์พุตลงใน journal ได้อยู่แล้ว การรัน Docker บน VPS จะคุ้มค่าเมื่อคุณต้องการให้อิมเมจที่ build เสร็จแล้วเป็นสิ่งที่คุณนำไป deploy จริงๆ

FAQ

ฉันสามารถใช้ pip install พร้อมกับแฟล็ก --break-system-packages ได้หรือไม่

ไม่ควรทำบนเซิร์ฟเวอร์ที่คุณต้องดูแลให้ทำงานได้อย่างต่อเนื่อง แฟล็กนี้ทำหน้าที่ตามชื่อของมัน คือการถอดตัวป้องกันออก แล้วให้ pip เขียนไฟล์ลงใน /usr/local/lib/python3.12/dist-packages ซึ่งมีลำดับความสำคัญสูงกว่าไดเรกทอรีของ apt ใน sys.path เวอร์ชันที่คุณติดตั้งจะไปทับซ้อน (shadow) เวอร์ชันของระบบสำหรับสคริปต์ทุกตัวที่ทำงานภายใต้ /usr/bin/python3 ในขณะที่ apt ยังคงเข้าใจว่าเวอร์ชันเดิมของมันถูกติดตั้งอยู่ จึงไม่มีระบบใดตรวจพบความขัดแย้งจนกว่าจะมีบางอย่างพังลง หากเป็นภายใน container image ที่คุณสร้างใหม่ตั้งแต่ต้นทุกครั้ง ความเสียหายจะจำกัดอยู่แค่ใน image นั้น จึงถือว่ายอมรับได้ในกรณีดังกล่าว แต่สำหรับเครื่องที่คุณต้องดูแลรักษา ให้สร้าง venv ขึ้นมาแทน ซึ่งใช้เพียงคำสั่งเดียวเท่านั้น

virtual environment ควรวางไว้ที่ใดบนเซิร์ฟเวอร์

ควรวางไว้ภายในไดเรกทอรีของแอปพลิเคชันเองในชื่อ /srv/myapp/.venv โดยให้ deploy user เป็นเจ้าของ และให้ service account มีสิทธิ์เพียงอ่านและเรียกใช้งาน (read and execute) เท่านั้น ควรแยก venv ต่อหนึ่งแอปพลิเคชัน เพราะการใช้ venv ร่วมกันอาจทำให้การอัปเกรดแอปพลิเคชันแรกส่งผลกระทบจนแอปพลิเคชันที่สองใช้งานไม่ได้ ห้ามย้ายหรือคัดลอก venv หลังจากสร้างเสร็จแล้ว เพราะสคริปต์ทุกตัวในไดเรกทอรี bin/ จะมีการระบุ absolute path ไว้ในบรรทัด shebang ดังนั้นหากย้าย venv ไปที่อื่นจะทำให้เกิดข้อผิดพลาด bad interpreter: No such file or directory ให้ลบแล้วสร้างใหม่จาก requirements.txt แทน

ทำไม systemd service ของฉันถึงล้มเหลวด้วยข้อผิดพลาด ModuleNotFoundError

เพราะ unit กำลังเรียกใช้งาน interpreter ที่ไม่ใช่ของ venv ให้รัน systemctl cat myapp แล้วอ่าน ExecStart โดย unit ต้องระบุ /srv/myapp/.venv/bin/python หรือ console script จากไดเรกทอรี bin/ เดียวกันนั้นด้วย absolute path การใช้คำสั่ง source activate ใน unit file ไม่สามารถทำได้ เพราะ ExecStart ไม่ใช่เชลล์ และ systemd จะรายงาน Failed to locate executable source พร้อมกับ status=203/EXEC ให้เพิ่ม Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin เพื่อให้ subprocess ใดๆ ที่โค้ดของคุณเรียกใช้งานสามารถค้นพบเครื่องมือของ venv ได้เช่นกัน

ฉันควรใช้ uv แทน venv และ pip หรือไม่

ให้ใช้ uv เมื่อคุณต้องการ lockfile, เมื่อเวลาที่ใช้ในการติดตั้งนานจนเป็นปัญหา หรือเมื่อคุณต้องการ Python เวอร์ชันที่ไม่มีใน distribution ของคุณ uv จะสร้าง venv แบบปกติขึ้นมา ดังนั้น unit ของ systemd และโครงสร้างไฟล์จึงไม่ต้องเปลี่ยนแปลง และ uv sync --frozen จะติดตั้งสิ่งที่ระบุไว้ใน lockfile อย่างแม่นยำ หากแอปพลิเคชันเดียวถูก deploy จาก git โดยใช้ requirements.txt ที่ระบุเวอร์ชันไว้ชัดเจนและใช้เวลาติดตั้งเพียงไม่กี่วินาที python3 -m venv ก็เพียงพอแล้ว และยังช่วยลดจำนวน binary ที่ต้องคอยอัปเดตบนเซิร์ฟเวอร์ลงไปหนึ่งตัว

#python#venv#pipx#uv#deployment