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

View File

@@ -76,6 +76,10 @@ done
# nvcc" even when the GPU itself is fully visible to the container.
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` /
# `sudo` because it cleans up the process tree (no extra shell layer)
# so signals (SIGTERM from `docker stop`) reach uvicorn directly.

View File

@@ -183,13 +183,41 @@ def _package_status_note(name: str, probe: dict) -> str:
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:
names_lit = ",".join(repr(n) for n in names)
return f"""
import importlib.util
import importlib.metadata as md
import json
import os
import shutil
import site
names=[{names_lit}]
dist_names={{
@@ -204,6 +232,24 @@ bin_names={{
'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):
spec = importlib.util.find_spec(n)
loader = getattr(spec, 'loader', None) if spec else None
@@ -793,6 +839,7 @@ def setup_shell_routes() -> APIRouter:
_require_admin(request)
_reject_cross_site(request)
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"):
_port = str(ssh_port).strip()
if not _SSH_PORT_RE.match(_port) or not (1 <= int(_port) <= 65535):

View File

@@ -3,6 +3,7 @@
import builtins
import importlib.util
import json
import os
import sys
from pathlib import Path
from types import SimpleNamespace
@@ -14,7 +15,9 @@ from routes.shell_routes import (
_running_in_container,
_docker_row_status,
_package_installed_from_probe,
_package_probe_script,
_package_status_note,
_prepend_user_install_bins_to_path,
_reject_cross_site,
_ssh_base_argv,
_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", 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:
def test_basic_host_no_port(self):