# This Dockerfile is provided for convenience if you wish to run Phoenix in a # container or sidecar. To build the image, run the following commmand: # # > docker build -t phoenix # # You can then run the image in the background with: # # > docker run -d --name phoenix -p 6006:6006 phoenix # # or in the foreground with: # # > docker run -it -p 6006:6006 phoenix # # How are you using Phoenix in production? Let us know! # # To get support or provide feedback, contact the team in the #phoenix-support # channel in the Arize AI Slack community or file an issue on GitHub: # # - https://join.slack.com/t/arize-ai/shared_invite/zt-3r07iavnk-ammtATWSlF0pSrd1DsMW7g # - https://github.com/Arize-ai/phoenix/issues ARG BASE_IMAGE=gcr.io/distroless/python3-debian13:nonroot # To deploy it on an arm64, like Raspberry Pi or Apple-Silicon, chose this image instead: # ARG BASE_IMAGE=gcr.io/distroless/python3-debian13:nonroot-arm64 # Two stages build on this image, so it is pinned once: they must resolve to # the same digest or the build pulls twice. Keep the uv version equal to the # lower bound of [tool.uv] required-version in pyproject.toml. ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.13.7-python3.13-trixie-slim # This Dockerfile is a multi-stage build. The first stage builds the frontend. FROM node:24-slim AS frontend-builder ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" ENV PHOENIX_ENABLE_SOURCE_MAP=True WORKDIR /phoenix/js/ COPY ./js /phoenix/js # The app's workspace dependency @arizeai/phoenix-client regenerates its # OpenAPI types from the repo-level schema during its build. COPY ./schemas /phoenix/schemas RUN npm i -g corepack RUN corepack enable RUN corepack install RUN pnpm install # Build the app (phoenix-ui) and its workspace dependencies in topological # order. The vite build writes to /phoenix/src/phoenix/server/static. RUN pnpm --filter 'phoenix-ui...' run build # The second stage builds the backend. FROM ${UV_IMAGE} AS backend-builder WORKDIR /phoenix COPY ./src /phoenix/src COPY ./pyproject.toml /phoenix/ COPY ./uv.lock /phoenix/ COPY ./LICENSE /phoenix/ COPY ./IP_NOTICE /phoenix/ COPY ./README.md /phoenix/ COPY --from=frontend-builder /phoenix/src/phoenix/server/static/ /phoenix/src/phoenix/server/static/ RUN uv sync \ --no-dev \ --no-install-project \ --no-sources \ --extra container \ --extra pg RUN uv build RUN uv pip install dist/*.whl --no-deps # Bundle the Deno runtime so the local DENO sandbox provider works inside # the distroless image. denoland/deno:bin- is a scratch-based # image that contains a single statically-linked /deno binary, which is # safe to COPY into the distroless final stage without dragging in a # shell or libc. Pinning the version (NOT :latest) keeps builds # reproducible. FROM denoland/deno:bin-2.1.4 AS deno-binary # Pre-download the CPython WASM binary so the WASM sandbox provider works # inside the distroless final image without network egress or a writable # home cache. The URL/filename/sha256 here MUST stay in sync with # src/phoenix/server/sandbox/_download.py (_WASM_URL / _WASM_FILENAME / # _WASM_SHA256); the env var PHOENIX_WASM_BINARY_PATH (set in the final # stage) is the authoritative resolver hook consumed by # ensure_wasm_binary(). The sha256 assertion guards against upstream # release-asset tampering — TLS alone is not enough for a binary that # executes user code in the sandbox. # # Its own stage, and it must stay that way: nothing here depends on the # repo, so the layer is keyed on the URL and digest alone and survives # every source change. Folded back into backend-builder it would sit below # COPY ./src and re-download on each commit. Built on UV_IMAGE — the image # the backend already pulls — for its python, so no apt-get curl layer and # no second pull. # # Retried with backoff: GitHub intermittently closes the connection to this # asset without sending a response. socket.setdefaulttimeout stands in for # the timeout argument urlretrieve does not take, and mirrors # _WASM_DOWNLOAD_TIMEOUT_SECONDS in _download.py. Verification runs inside # the loop and is the only proof of a complete transfer — urlretrieve # reports success on a clean close when no Content-Length was received — so # the loop breaks only on a digest match and no unverified file survives an # attempt. FROM ${UV_IMAGE} AS wasm-runtime RUN mkdir -p /wasm \ && for delay in 5 10 20 40 stop; do \ python -c "import hashlib, os, socket, sys, urllib.request; \ socket.setdefaulttimeout(30); \ dest = '/wasm/python-3.12.0.wasm'; \ expected = 'e5dc5a398b07b54ea8fdb503bf68fb583d533f10ec3f930963e02b9505f7a763'; \ urllib.request.urlretrieve( \ 'https://github.com/vmware-labs/webassembly-language-runtimes/releases/download/python%2F3.12.0%2B20231211-040d5a6/python-3.12.0.wasm', \ dest); \ actual = hashlib.sha256(open(dest, 'rb').read()).hexdigest(); \ (actual == expected) or (os.remove(dest), \ sys.exit(f'SHA-256 mismatch for {dest}: expected {expected}, got {actual}'))" && break; \ [ "$delay" = stop ] && { echo 'WASM binary fetch failed after 5 attempts' >&2; exit 1; }; \ echo "WASM binary fetch failed; retrying in ${delay}s" >&2; \ sleep "$delay"; \ done # The production image is distroless, meaning that it is a minimal image that # contains only the necessary dependencies to run the application. This is # useful for security and performance reasons. If you need to debug the # container, you can build from the debug image instead and run # # > docker run --entrypoint=sh -it phoenix # # to enter a shell. For more information, see: # # https://github.com/GoogleContainerTools/distroless?tab=readme-ov-file#debug-images # # Use the debug tag in the following line to build the debug image. FROM ${BASE_IMAGE} WORKDIR /phoenix COPY --from=backend-builder /phoenix/.venv/ ./.venv # Bundled local sandbox runtimes (Deno + CPython WASM). The Deno binary # is statically linked, so a single COPY into the distroless image keeps # the security/footprint properties documented in the comment block # above. shutil.which("deno") in src/phoenix/server/sandbox/deno_backend.py # resolves to /usr/local/bin/deno because /usr/local/bin is on the # distroless image's default PATH (inherited via the base image's ENV). COPY --chmod=755 --from=deno-binary /deno /usr/local/bin/deno COPY --from=wasm-runtime /wasm/python-3.12.0.wasm /opt/phoenix/wasm/python-3.12.0.wasm ENV PHOENIX_WASM_BINARY_PATH=/opt/phoenix/wasm/python-3.12.0.wasm # Ensure /usr/local/bin is on PATH so shutil.which("deno") in # deno_backend.py resolves to the bundled binary above. The base # distroless image's default PATH already includes /usr/local/bin, but # we set it explicitly to keep DENO discovery insensitive to base-image # PATH drift on future bumps. # # /phoenix/.venv/bin is appended so console scripts shipped by wheels are # discoverable — today the `monty` binary from pydantic-monty-runtime, # which the Monty sandbox and MCP code mode spawn as a worker process. # The ENTRYPOINT runs the *system* interpreter with the venv's # site-packages on PYTHONPATH rather than the venv's own interpreter, so # sysconfig.get_path("scripts") reports /usr/local/bin and never the # venv's bin; PATH is the only lookup left that can reach it (see # pydantic_monty._binary.find_monty_binary). Appended rather than # prepended because the venv's python/python3/python3.13 symlinks point # at the builder image's interpreter and dangle here — the system # interpreter must keep winning any name collision. ENV PATH="/usr/local/bin:/usr/bin:/bin:/phoenix/.venv/bin" ENV PYTHONPATH="/phoenix/.venv/lib/python3.13/site-packages:$PYTHONPATH" ENV PYTHONUNBUFFERED=1 # Bind all IPv4 and IPv6 interfaces (dual-stack) for HTTP, gRPC, and Prometheus. ENV PHOENIX_HOST=:: # Expose the Phoenix port. EXPOSE 6006 # Expose the Phoenix gRPC port. EXPOSE 4317 # Expose the Prometheus port. EXPOSE 9090 # Run the Phoenix server. Note that the ENTRYPOINT of the base image invokes # Python, so no explicit invocation of Python is needed here. See # https://github.com/GoogleContainerTools/distroless/blob/16dc4a6a33838006fe956e4c19f049ece9c18a8d/python3/BUILD#L55 CMD ["-m", "phoenix.server.main", "serve"]