fix(docker): bundle Python runtime for portable /agent-server#2678
fix(docker): bundle Python runtime for portable /agent-server#2678simonrosenberg wants to merge 1 commit intomainfrom
Conversation
After building the venv with system Python, copy the interpreter binary, standard library, and libpython shared objects into /agent-server/.python/. Re-point the venv symlinks and pyvenv.cfg at the bundled copy so that the entire /agent-server directory is self-contained. This means eval images (and any other consumer) can COPY /agent-server onto any base image — even one without Python at /usr/local/bin — and the entrypoint will resolve. Without this fix, commit0 eval images (Ubuntu 22.04 base, Python at /usr/bin/python3) fail to start because the venv symlinks point to the builder's /usr/local/bin/python3 which doesn't exist in the target image. Fixes OpenHands/benchmarks#607 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
Update SDK submodule to include the Python runtime bundling fix (OpenHands/software-agent-sdk#2678). The agent-server Dockerfile now bundles Python into /agent-server/.python/, making the venv portable across base images. This fixes commit0 evaluations where all runtime pods were stuck in pending because the agent-server container couldn't start — the venv symlinked to /usr/local/bin/python3 which doesn't exist in commit0's Ubuntu 22.04 base images. Fixes #607 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
all-hands-bot
left a comment
There was a problem hiding this comment.
Taste Rating: 🟡 Acceptable - Pragmatic fix for a real production issue, but adds build complexity without automated verification.
Key Insight: This solves the immediate portability problem, but the solution's robustness depends on several implicit assumptions about Python's installation structure that could break in future Debian releases.
| RUN set -eux; \ | ||
| REAL_PYTHON=$(readlink -f .venv/bin/python3); \ | ||
| PY_VER=$("${REAL_PYTHON}" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"); \ | ||
| PYTHON_PREFIX=$("${REAL_PYTHON}" -c "import sys; print(sys.base_prefix)"); \ | ||
| # --- copy interpreter binary ------------------------------------------------- \ |
There was a problem hiding this comment.
🟠 Important - Robustness: This loop assumes libpython*.so* files exist and follow Debian's naming convention. If no libraries match (unlikely but possible in minimal Python builds), the loop silently succeeds.
Consider adding a verification step after the loop:
if ! ls .python/lib/libpython*.so* 1>/dev/null 2>&1; then \
echo "ERROR: No libpython shared libraries found"; \
exit 1; \
fi;| mkdir -p .python/lib; \ | ||
| cp -a "${PYTHON_PREFIX}/lib/python${PY_VER}" ".python/lib/python${PY_VER}"; \ | ||
| rm -rf ".python/lib/python${PY_VER}/test" \ | ||
| ".python/lib/python${PY_VER}/tests" \ | ||
| ".python/lib/python${PY_VER}/idle_test" \ | ||
| ".python/lib/python${PY_VER}/idlelib"; \ | ||
| # --- copy shared libraries (libpython) --------------------------------------- \ | ||
| for lib in "${PYTHON_PREFIX}"/lib/libpython*.so*; do \ |
There was a problem hiding this comment.
🟡 Suggestion - Defensive Programming: This loop assumes all python* files in .venv/bin/ are symlinks (the || continue handles non-symlinks). While this should be true for uv-created venvs, it's an implicit assumption.
The subsequent explicit checks (lines 80-81) are good defensive programming, but consider documenting this assumption in a comment.
| [ -L .venv/bin/python3 ] || ln -s "../../.python/bin/python3" .venv/bin/python3; \ | ||
| sed -i "s|^home = .*|home = /agent-server/.python/bin|" .venv/pyvenv.cfg; \ | ||
| # --- quick smoke-test inside the builder ------------------------------------- \ | ||
| .venv/bin/python -c "import sys; print('bundled python:', sys.executable, sys.version)" |
There was a problem hiding this comment.
🟠 Important - Testing Gap: The smoke test only verifies the interpreter runs and can import sys. It doesn't verify:
- Critical stdlib modules (e.g.,
import ssl, json, urllib) - That the bundled libpython is actually being used
- That the venv can install/import packages
Consider a more comprehensive smoke test:
.venv/bin/python -c "import sys, ssl, json, urllib.request; import openhands.agent_server; print('✓ bundled python:', sys.executable, sys.version)"| FROM base-image AS source | ||
| ARG USERNAME | ||
| COPY --chown=${USERNAME}:${USERNAME} --from=builder /agent-server /agent-server | ||
| # Bundled Python's libpython*.so lives under /agent-server/.python/lib |
There was a problem hiding this comment.
🟡 Suggestion - Documentation: The LD_LIBRARY_PATH addition is critical for the bundled libpython to work. Consider adding a comment explaining:
- Why this is needed (bundled libpython.so)
- Security consideration (directory is owned by ${USERNAME}, not world-writable)
| # Bundle the Python runtime inside /agent-server so that the entire directory | ||
| # is self-contained and portable. Eval images (and any other consumer) can | ||
| # COPY /agent-server onto *any* base image without requiring that base image | ||
| # to ship a compatible system Python. | ||
| # | ||
| # What we copy: | ||
| # .python/bin/python3.13 – the interpreter binary | ||
| # .python/lib/python3.13/ – the standard library (minus tests) | ||
| # .python/lib/libpython*.so* – shared libraries (Debian builds --enable-shared) | ||
| # | ||
| # We then repoint the venv's symlinks and pyvenv.cfg at the bundled copy. | ||
| RUN set -eux; \ | ||
| REAL_PYTHON=$(readlink -f .venv/bin/python3); \ | ||
| PY_VER=$("${REAL_PYTHON}" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"); \ | ||
| PYTHON_PREFIX=$("${REAL_PYTHON}" -c "import sys; print(sys.base_prefix)"); \ | ||
| # --- copy interpreter binary ------------------------------------------------- \ | ||
| mkdir -p .python/bin; \ | ||
| cp "${REAL_PYTHON}" ".python/bin/python${PY_VER}"; \ | ||
| ln -s "python${PY_VER}" .python/bin/python3; \ | ||
| ln -s "python${PY_VER}" .python/bin/python; \ | ||
| # --- copy standard library (skip test suite to save ~30 MB) ------------------ \ | ||
| mkdir -p .python/lib; \ | ||
| cp -a "${PYTHON_PREFIX}/lib/python${PY_VER}" ".python/lib/python${PY_VER}"; \ | ||
| rm -rf ".python/lib/python${PY_VER}/test" \ | ||
| ".python/lib/python${PY_VER}/tests" \ | ||
| ".python/lib/python${PY_VER}/idle_test" \ | ||
| ".python/lib/python${PY_VER}/idlelib"; \ | ||
| # --- copy shared libraries (libpython) --------------------------------------- \ | ||
| for lib in "${PYTHON_PREFIX}"/lib/libpython*.so*; do \ | ||
| [ -e "$lib" ] && cp -a "$lib" .python/lib/; \ | ||
| done; \ | ||
| # --- repoint venv at the bundled Python -------------------------------------- \ | ||
| for f in .venv/bin/python*; do \ | ||
| [ -L "$f" ] || continue; \ | ||
| name=$(basename "$f"); \ | ||
| rm "$f"; \ | ||
| ln -s "../../.python/bin/${name}" "$f"; \ | ||
| done; \ | ||
| # Ensure canonical names resolve (some venvs only create python3 + python) \ | ||
| [ -L .venv/bin/python ] || ln -s "../../.python/bin/python" .venv/bin/python; \ | ||
| [ -L .venv/bin/python3 ] || ln -s "../../.python/bin/python3" .venv/bin/python3; \ | ||
| sed -i "s|^home = .*|home = /agent-server/.python/bin|" .venv/pyvenv.cfg; \ | ||
| # --- quick smoke-test inside the builder ------------------------------------- \ | ||
| .venv/bin/python -c "import sys; print('bundled python:', sys.executable, sys.version)" |
There was a problem hiding this comment.
🟡 Suggestion - Maintainability: This 45-line shell script could be extracted into a separate script file (e.g., bundle-python.sh) and copied/executed. Benefits:
- Easier to read the Dockerfile
- Easier to test the script in isolation
- Easier to maintain and debug
Not blocking, but worth considering for future refactoring.
Summary
/agent-server/.python/and repoint the venv symlinks at it/agent-serverfully self-contained — eval images can COPY it onto any base image without needing Python at/usr/local/bin/python3--python-preference only-systemto avoid the seccomp/executable-stack issue with python-build-standaloneProblem
SDK v1.15.0 (commit 06b9186) switched from uv-managed Python to
--python-preference only-system, which creates a venv with symlinks to/usr/local/bin/python3(from thepython:3.13-bookwormbuilder). When this venv is COPYed onto commit0 base images (Ubuntu 22.04, Python at/usr/bin/python3), the symlink is broken and the container fails to start:This causes all commit0 evaluation pods to get stuck in "pending" status (OpenHands/benchmarks#607).
SWE-bench was unaffected because its base images derive from Python Docker images that have
/usr/local/bin/python3.Test plan
docker.io/wentingzhao/tinydb:v0) — container starts and Python resolves correctly.python/directoryPython: /agent-server/.venv/bin/python 3.13.12Fixes OpenHands/benchmarks#607
🤖 Generated with Claude Code
Agent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:d7b6700-pythonRun
All tags pushed for this build
About Multi-Architecture Support
d7b6700-python) is a multi-arch manifest supporting both amd64 and arm64d7b6700-python-amd64) are also available if needed