Expose Cookbook user-install CLIs in Docker (#887)

Ensure pip --user console scripts like vLLM are visible to Docker
runtime and dependency probes by adding the user install bin directory
to PATH.
This commit is contained in:
Juan Pablo Jiménez
2026-06-01 22:23:29 -05:00
committed by GitHub
parent 9a1893760d
commit e58e4a185d
3 changed files with 74 additions and 0 deletions
+4
View File
@@ -76,6 +76,10 @@ done
# nvcc" even when the GPU itself is fully visible to the container. # nvcc" even when the GPU itself is fully visible to the container.
export VLLM_USE_FLASHINFER_SAMPLER="${VLLM_USE_FLASHINFER_SAMPLER:-0}" export VLLM_USE_FLASHINFER_SAMPLER="${VLLM_USE_FLASHINFER_SAMPLER:-0}"
# Make Cookbook-installed Python CLIs visible after `pip install --user`.
# vLLM and helper scripts land here because /app is the non-root user's HOME.
export PATH="/app/.local/bin:$PATH"
# Drop root and run the actual app. `gosu` is preferred over `su` / # Drop root and run the actual app. `gosu` is preferred over `su` /
# `sudo` because it cleans up the process tree (no extra shell layer) # `sudo` because it cleans up the process tree (no extra shell layer)
# so signals (SIGTERM from `docker stop`) reach uvicorn directly. # so signals (SIGTERM from `docker stop`) reach uvicorn directly.
+47
View File
@@ -183,13 +183,41 @@ def _package_status_note(name: str, probe: dict) -> str:
return "" return ""
def _prepend_user_install_bins_to_path() -> None:
"""Make pip --user console scripts visible to dependency probes.
Docker Cookbook installs vLLM with `python -m pip install --user`, which
drops the `vllm` CLI in /app/.local/bin. The running app process does not
inherit that PATH update, so `shutil.which("vllm")` can report missing even
after a successful install.
"""
try:
import site
candidates = [os.path.join(site.USER_BASE, "bin")]
except Exception:
candidates = []
candidates.append(os.path.expanduser("~/.local/bin"))
parts = os.environ.get("PATH", "").split(os.pathsep) if os.environ.get("PATH") else []
changed = False
for path in reversed([p for p in candidates if p]):
if path not in parts:
parts.insert(0, path)
changed = True
if changed:
os.environ["PATH"] = os.pathsep.join(parts)
def _package_probe_script(names: list[str]) -> str: def _package_probe_script(names: list[str]) -> str:
names_lit = ",".join(repr(n) for n in names) names_lit = ",".join(repr(n) for n in names)
return f""" return f"""
import importlib.util import importlib.util
import importlib.metadata as md import importlib.metadata as md
import json import json
import os
import shutil import shutil
import site
names=[{names_lit}] names=[{names_lit}]
dist_names={{ dist_names={{
@@ -204,6 +232,24 @@ bin_names={{
'llama_cpp':['llama-server'], 'llama_cpp':['llama-server'],
}} }}
def add_user_install_bins_to_path():
candidates = []
try:
candidates.append(os.path.join(site.USER_BASE, 'bin'))
except Exception:
pass
candidates.append(os.path.expanduser('~/.local/bin'))
parts = os.environ.get('PATH', '').split(os.pathsep) if os.environ.get('PATH') else []
changed = False
for path in reversed([p for p in candidates if p]):
if path not in parts:
parts.insert(0, path)
changed = True
if changed:
os.environ['PATH'] = os.pathsep.join(parts)
add_user_install_bins_to_path()
def mod_status(n): def mod_status(n):
spec = importlib.util.find_spec(n) spec = importlib.util.find_spec(n)
loader = getattr(spec, 'loader', None) if spec else None loader = getattr(spec, 'loader', None) if spec else None
@@ -793,6 +839,7 @@ def setup_shell_routes() -> APIRouter:
_require_admin(request) _require_admin(request)
_reject_cross_site(request) _reject_cross_site(request)
import importlib, importlib.metadata as importlib_metadata, shlex, json as _json import importlib, importlib.metadata as importlib_metadata, shlex, json as _json
_prepend_user_install_bins_to_path()
if ssh_port and str(ssh_port).strip() not in ("", "22"): if ssh_port and str(ssh_port).strip() not in ("", "22"):
_port = str(ssh_port).strip() _port = str(ssh_port).strip()
if not _SSH_PORT_RE.match(_port) or not (1 <= int(_port) <= 65535): if not _SSH_PORT_RE.match(_port) or not (1 <= int(_port) <= 65535):
+23
View File
@@ -3,6 +3,7 @@
import builtins import builtins
import importlib.util import importlib.util
import json import json
import os
import sys import sys
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
@@ -14,7 +15,9 @@ from routes.shell_routes import (
_running_in_container, _running_in_container,
_docker_row_status, _docker_row_status,
_package_installed_from_probe, _package_installed_from_probe,
_package_probe_script,
_package_status_note, _package_status_note,
_prepend_user_install_bins_to_path,
_reject_cross_site, _reject_cross_site,
_ssh_base_argv, _ssh_base_argv,
_venv_activate_prefix, _venv_activate_prefix,
@@ -247,6 +250,26 @@ class TestPackageProbeStatus:
assert _package_installed_from_probe("diffusers", missing_torch) is False assert _package_installed_from_probe("diffusers", missing_torch) is False
assert _package_installed_from_probe("diffusers", ready) is True assert _package_installed_from_probe("diffusers", ready) is True
def test_local_user_install_bin_is_added_to_path(self, monkeypatch, tmp_path):
user_base = tmp_path / "user-base"
monkeypatch.setattr("site.USER_BASE", str(user_base))
monkeypatch.setenv("HOME", str(tmp_path / "home"))
monkeypatch.setenv("PATH", "/usr/bin")
_prepend_user_install_bins_to_path()
parts = os.environ["PATH"].split(os.pathsep)
assert str(user_base / "bin") in parts
assert str(tmp_path / "home" / ".local" / "bin") in parts
def test_remote_package_probe_checks_user_install_bin(self):
script = _package_probe_script(["vllm"])
assert "site.USER_BASE" in script
assert "os.path.expanduser('~/.local/bin')" in script
assert "add_user_install_bins_to_path()" in script
assert "shutil.which(b)" in script
class TestSshBaseArgv: class TestSshBaseArgv:
def test_basic_host_no_port(self): def test_basic_host_no_port(self):