Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
base_image:
description: Base runtime image
type: string
default: nikolaik/python-nodejs:python3.13-nodejs22-slim
default: python-node-runtime
image:
description: GHCR image name
type: string
Expand Down Expand Up @@ -285,7 +285,7 @@ jobs:
image_flavor: default
acp_provider_flavor: default
arch: amd64
base_image: nikolaik/python-nodejs:python3.13-nodejs22-slim
base_image: python-node-runtime
runner: ubuntu-24.04
platform: linux/amd64

Expand All @@ -294,7 +294,7 @@ jobs:
image_flavor: default
acp_provider_flavor: default
arch: arm64
base_image: nikolaik/python-nodejs:python3.13-nodejs22-slim
base_image: python-node-runtime
runner: ubuntu-24.04-arm
platform: linux/arm64

Expand All @@ -304,7 +304,7 @@ jobs:
image_flavor: slim
acp_provider_flavor: none
arch: amd64
base_image: nikolaik/python-nodejs:python3.13-nodejs22-slim
base_image: python-node-runtime
runner: ubuntu-24.04
platform: linux/amd64

Expand All @@ -313,7 +313,7 @@ jobs:
image_flavor: slim
acp_provider_flavor: none
arch: arm64
base_image: nikolaik/python-nodejs:python3.13-nodejs22-slim
base_image: python-node-runtime
runner: ubuntu-24.04-arm
platform: linux/arm64

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/typescript-client-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
uv sync --frozen
uv run ./openhands-agent-server/openhands/agent_server/docker/build.py \
--build-ctx-only --arch amd64 \
--base-image nikolaik/python-nodejs:python3.13-nodejs22-slim
--base-image python-node-runtime

- name: Build agent-server image from this branch
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
Expand All @@ -87,7 +87,7 @@ jobs:
# Scope server.yml populates with mode=max on push to main.
cache-from: type=gha,scope=agent-server-python-amd64
build-args: |
BASE_IMAGE=nikolaik/python-nodejs:python3.13-nodejs22-slim
BASE_IMAGE=python-node-runtime
OPENHANDS_BUILD_GIT_SHA=${{ github.event.pull_request.head.sha || github.sha }}
OPENHANDS_BUILD_GIT_REF=${{ github.head_ref != '' && format('refs/heads/{0}', github.head_ref) || github.ref }}
INSTALL_ACP_PROVIDERS=claude-code,codex,gemini-cli
Expand Down
129 changes: 96 additions & 33 deletions openhands-agent-server/openhands/agent_server/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# NOTE: LC_ALL/LANG must be set to C.UTF-8 for libtmux to work correctly with
# PyInstaller builds. Without proper locale, tmux converts UTF-8 separator
# characters to underscores, breaking libtmux's format parsing.
ARG BASE_IMAGE=nikolaik/python-nodejs:python3.13-nodejs22-slim
ARG BASE_IMAGE=python-node-runtime
ARG USERNAME=openhands
ARG UID=10001
ARG GID=10001
Expand All @@ -26,6 +26,52 @@ ARG INSTALL_ACP_PROVIDERS=claude-code,codex,gemini-cli
# below is written so that's a one-case addition, not a signature change.
ARG INSTALL_CAPABILITIES=vscode,browser,docker

####################################################################################
# Canonical Python + Node runtime
#
# Start from Debian itself and copy only the language runtimes from their official
# images. The final stage installs only the shared libraries required by Python
# and Node; developer and package-manager tooling is added by the agent-server
# stages below when needed.
####################################################################################
FROM python:3.13.15-slim-trixie AS python-runtime
FROM node:24.21.0-trixie-slim AS node-runtime
FROM debian:trixie-slim AS python-node-runtime

COPY --from=python-runtime /usr/local /usr/local
COPY --from=node-runtime /usr/local/bin/node /usr/local/bin/node
COPY --from=node-runtime /usr/local/lib/node_modules /usr/local/lib/node_modules

RUN set -eux; \
ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm; \
ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx; \
ln -s ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack; \
apt-get update; \
apt-get upgrade -y; \
apt-get install -y --no-install-recommends \
ca-certificates \
libbz2-1.0 \
libdb5.3t64 \
libffi8 \
libgcc-s1 \
libgdbm6t64 \
liblzma5 \
libncursesw6 \
libreadline8t64 \
libsqlite3-0 \
libssl3t64 \
libstdc++6 \
libtinfo6 \
libuuid1 \
libzstd1 \
netbase \
tzdata \
zlib1g; \
rm -rf /var/lib/apt/lists/*; \
python3 --version; \
node --version; \
npm --version

####################################################################################
# Builder (source mode)
# We copy source + build a venv here for local dev and debugging.
Expand Down Expand Up @@ -168,9 +214,8 @@ RUN set -eux; \

####################################################################################
# Base image (minimal)
# It includes only basic packages and the UV runtime.
# No Docker, no browser, no VSCode Web.
# Suitable for running in headless/evaluation mode.
# It includes only the packages required to run the agent server and its Bash
# and Git APIs. Development tools and optional capabilities live in base-image.
####################################################################################
FROM ${BASE_IMAGE} AS base-image-minimal
ARG USERNAME UID GID PORT
Expand All @@ -192,36 +237,22 @@ RUN set -eux; \
if command -v apt-get >/dev/null 2>&1; then \
apt-get -o Acquire::Retries=5 update; \
apt-get -o Acquire::Retries=5 install -y --no-install-recommends \
bash ca-certificates curl wget sudo apt-utils git jq tmux tar \
build-essential coreutils util-linux procps findutils grep sed \
tini apt-transport-https gnupg lsb-release xz-utils; \
bash ca-certificates git tini tmux; \
rm -rf /var/lib/apt/lists/*; \
elif command -v apk >/dev/null 2>&1; then \
apk add --no-cache \
bash ca-certificates curl wget sudo git jq tmux tar build-base \
coreutils util-linux procps findutils grep sed tini gnupg shadow xz; \
apk add --no-cache bash ca-certificates git shadow tini tmux; \
elif command -v microdnf >/dev/null 2>&1; then \
microdnf install -y \
bash ca-certificates curl wget sudo git jq tmux tar make gcc gcc-c++ \
coreutils util-linux procps-ng findutils grep sed shadow-utils \
gnupg2 xz; \
microdnf install -y bash ca-certificates git shadow-utils tmux; \
microdnf clean all; \
elif command -v dnf >/dev/null 2>&1; then \
dnf install -y \
bash ca-certificates curl wget sudo git jq tmux tar make gcc gcc-c++ \
coreutils util-linux procps-ng findutils grep sed shadow-utils \
gnupg2 xz; \
dnf install -y bash ca-certificates git shadow-utils tmux; \
dnf clean all; \
elif command -v yum >/dev/null 2>&1; then \
yum install -y \
bash ca-certificates curl wget sudo git jq tmux tar make gcc gcc-c++ \
coreutils util-linux procps-ng findutils grep sed shadow-utils \
gnupg2 xz; \
yum install -y bash ca-certificates git shadow-utils tmux; \
yum clean all; \
elif command -v zypper >/dev/null 2>&1; then \
zypper --non-interactive install --no-recommends \
bash ca-certificates curl wget sudo git jq tmux tar make gcc gcc-c++ \
coreutils util-linux procps findutils grep sed shadow gpg2 xz; \
bash ca-certificates git shadow tmux; \
zypper clean --all; \
else \
echo "Unsupported base image: no known package manager found" >&2; \
Expand All @@ -230,11 +261,6 @@ RUN set -eux; \
grep -Eq "^[^:]*:[^:]*:${GID}:" /etc/group || groupadd -g "${GID}" "${USERNAME}"; \
grep -Eq "^${USERNAME}:" /etc/passwd || \
useradd -m -u "${UID}" -g "${GID}" -s /bin/bash "${USERNAME}"; \
# Best-effort: add user to a sudo group when one exists (Debian-style
# `sudo` group). On Alpine/RHEL/SUSE there is no `sudo` group by default,
# and the NOPASSWD sudoers line below grants sudo regardless of group.
usermod -aG sudo "${USERNAME}" 2>/dev/null || true; \
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers; \
mkdir -p /workspace/project; \
chown -R "${USERNAME}:${USERNAME}" /workspace

Expand Down Expand Up @@ -274,10 +300,6 @@ RUN if ! "$ACP_NODE_DIR/bin/node" --version >/dev/null 2>&1; then \
RUN mkdir -p /etc/claude-code && \
echo '{"permissions":{"allow":["Edit","Read","Bash"]}}' > /etc/claude-code/managed-settings.json

# NOTE: we should NOT include UV_PROJECT_ENVIRONMENT here,
# since the agent might use it to perform other work (e.g. tools that use Python)
COPY --from=ghcr.io/astral-sh/uv:0.11.6 /uv /uvx /bin/

USER ${USERNAME}
WORKDIR /
# Locale settings required for libtmux to work with PyInstaller builds
Expand All @@ -299,6 +321,47 @@ ARG INSTALL_CAPABILITIES

USER root

# Full images are general-purpose development environments. Keep compilers,
# package/repository helpers, and command-line utilities out of binary-minimal.
RUN set -eux; \
if command -v apt-get >/dev/null 2>&1; then \
apt-get -o Acquire::Retries=5 update; \
apt-get -o Acquire::Retries=5 install -y --no-install-recommends \
apt-transport-https apt-utils build-essential coreutils curl \
findutils gnupg grep jq lsb-release procps sed sudo tar util-linux \
wget xz-utils; \
rm -rf /var/lib/apt/lists/*; \
elif command -v apk >/dev/null 2>&1; then \
apk add --no-cache \
build-base coreutils curl findutils gnupg grep jq procps sudo tar \
util-linux wget xz; \
elif command -v microdnf >/dev/null 2>&1; then \
microdnf install -y \
coreutils curl findutils gcc gcc-c++ gnupg2 grep jq make procps-ng \
sed sudo tar util-linux wget xz; \
microdnf clean all; \
elif command -v dnf >/dev/null 2>&1; then \
dnf install -y \
coreutils curl findutils gcc gcc-c++ gnupg2 grep jq make procps-ng \
sed sudo tar util-linux wget xz; \
dnf clean all; \
elif command -v yum >/dev/null 2>&1; then \
yum install -y \
coreutils curl findutils gcc gcc-c++ gnupg2 grep jq make procps-ng \
sed sudo tar util-linux wget xz; \
yum clean all; \
elif command -v zypper >/dev/null 2>&1; then \
zypper --non-interactive install --no-recommends \
coreutils curl findutils gcc gcc-c++ gpg2 grep jq make procps sed \
sudo tar util-linux wget xz; \
zypper clean --all; \
fi; \
usermod -aG sudo "${USERNAME}" 2>/dev/null || true; \
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# Keep uv as part of the full development environment, but not binary-minimal.
COPY --from=ghcr.io/astral-sh/uv:0.11.6 /uv /uvx /bin/

# Fail fast on a typo'd capability name before spending any time on the
# installs below (each block only checks for ITS OWN keyword's presence, so
# an unrecognized one would otherwise be silently ignored rather than erroring).
Expand Down
8 changes: 2 additions & 6 deletions openhands-agent-server/openhands/agent_server/docker/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,9 +393,7 @@ def _package_version() -> str:


class BuildOptions(BaseModel):
# NOTE: Using Python 3.12 due to PyInstaller+libtmux compatibility issue
# with Python 3.13. See issue #1886 for details.
base_image: str = Field(default="nikolaik/python-nodejs:python3.12-nodejs22-slim")
base_image: str = Field(default="python-node-runtime")
custom_tags: str = Field(
default="", description="Comma-separated list of custom tags."
)
Expand Down Expand Up @@ -1069,9 +1067,7 @@ def main(argv: list[str]) -> int:
)
parser.add_argument(
"--base-image",
# NOTE: Using Python 3.12 due to PyInstaller+libtmux compatibility issue
# with Python 3.13. See issue #1886.
default=_env("BASE_IMAGE", "nikolaik/python-nodejs:python3.12-nodejs22-slim"),
default=_env("BASE_IMAGE", "python-node-runtime"),
help="Base image to use (default from $BASE_IMAGE).",
)
parser.add_argument(
Expand Down
24 changes: 24 additions & 0 deletions tests/cross/test_agent_server_build_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,30 @@ def test_agent_server_binary_copies_openhands_distribution_metadata() -> None:
assert f'*copy_metadata("{distribution}")' in spec_text


def test_python_image_uses_canonical_minimal_runtime() -> None:
dockerfile_text = AGENT_SERVER_DOCKERFILE.read_text(encoding="utf-8")
workflow_text = SERVER_WORKFLOW.read_text(encoding="utf-8")

assert "FROM debian:trixie-slim AS python-node-runtime" in dockerfile_text
assert "FROM python:3.13.15-slim-trixie AS python-runtime" in dockerfile_text
assert "FROM node:24.21.0-trixie-slim AS node-runtime" in dockerfile_text
assert "ARG BASE_IMAGE=python-node-runtime" in dockerfile_text
assert "apt-get update; \\\n apt-get upgrade -y;" in dockerfile_text
minimal_stage = "FROM ${BASE_IMAGE} AS base-image-minimal"
full_stage = "FROM base-image-minimal AS base-image"
minimal_packages = dockerfile_text.partition(minimal_stage)[2].partition(
full_stage
)[0]
full_packages = dockerfile_text.partition(full_stage)[2]
assert "build-essential" not in minimal_packages
assert "COPY --from=ghcr.io/astral-sh/uv" not in minimal_packages
assert "build-essential" in full_packages
assert "COPY --from=ghcr.io/astral-sh/uv" in full_packages
assert "nikolaik/python-nodejs" not in dockerfile_text
assert "base_image: python-node-runtime" in workflow_text
assert "nikolaik/python-nodejs" not in workflow_text


def test_agent_server_dockerfile_has_no_hardcoded_acp_packages() -> None:
"""The acp-providers stage must resolve packages/versions from the
dependency-free catalog at build time, not from Dockerfile-baked arms.
Expand Down
Loading