Skip to content

FEAT: production-ready Docker Compose deployment with offline private PyPI profile#5174

Open
OliverBryant wants to merge 4 commits into
xorbitsai:mainfrom
OliverBryant:feat/docker-compose-offline-deployment
Open

FEAT: production-ready Docker Compose deployment with offline private PyPI profile#5174
OliverBryant wants to merge 4 commits into
xorbitsai:mainfrom
OliverBryant:feat/docker-compose-offline-deployment

Conversation

@OliverBryant

Copy link
Copy Markdown
Collaborator

What do these changes do?

The existing docker-compose.yml / docker-compose-distributed.yml were added as bare examples and are not referenced anywhere in the documentation. This PR turns the standalone compose file into a first-class, documented deployment option, with built-in support for offline / air-gapped environments.

Reworked docker-compose.yml

  • All settings parameterized with ${VAR:-default} and an .env.example template (image tag, port, model source, shm size, log level, cache locations).
  • /status healthcheck (via python3 + urllib, since not every image variant ships curl) and restart: unless-stopped.
  • Model caches persisted in named volumes by default, overridable to host paths.
  • Removed the deprecated version field and the stale commented-out blocks.

New: offline profile with a private PyPI server

docker compose --profile offline up -d additionally starts a pypiserver service backed by a local ./wheels directory. The offline configuration (offline.env + mounted pip.conf) routes every runtime package install of the model virtual-env machinery to the private index through two complementary channels:

  • pip.conf feeds the inherit_pip_config mechanism (get_pip_config_args() only reads global.* keys from pip config list, so env vars alone are not picked up). A non-empty extra-index-url also prevents the hardcoded public vLLM/SGLang extra indexes from being injected (apply_engine_virtualenv_settings only fills them when unset).
  • UV_DEFAULT_INDEX / UV_INDEX_URL / UV_INSECURE_HOST cover uv invocations that do not carry index flags, e.g. the dependency-resolution dry-run used by XINFERENCE_VIRTUAL_ENV_SKIP_INSTALLED.

Model weights are handled by HF_HUB_OFFLINE=1 / TRANSFORMERS_OFFLINE=1 plus the mounted cache volumes. Users who prefer fully pre-provisioned images can set XINFERENCE_ENABLE_VIRTUAL_ENV=0 instead.

Other additions

  • docker-compose.cpu.yml: 4-line override for CPU-only hosts (-cpu image + deploy: !reset null).
  • New documentation page getting_started/using_docker_compose.rst (added to the toctree) covering quick start, configuration, CPU-only, and a step-by-step offline guide, with zh_CN translation.
  • .gitignore: keep the wheels/ mount point (.gitkeep), ignore user-local offline.env.

Online users are unaffected: docker compose up -d works with zero configuration, and the pypiserver service only starts when the offline profile is enabled.

Verification

  • docker compose config validated for all three views (default / --profile offline / CPU override).
  • Real smoke test on Docker Desktop (macOS, CPU): stack becomes healthy; inside the container pip config list exposes the global.* keys; requesting a version absent from the local index fails without falling back to pypi.org, proving uv resolves against the private index only.
  • End-to-end: launched qwen3 0.6B (transformers engine) through the compose stack on a slim image without torch/transformers — the runtime virtual-env installed 34 packages and the model came up successfully.
  • Docs: make html and make html_zh_cn build without new warnings.

Notes

  • Requires Docker Compose >= v2.24.4 (env_file: required: false, !reset); stated in the docs and file comments.
  • The distributed compose file is intentionally left untouched in this PR.

… PyPI profile

Rework the standalone docker-compose.yml into a first-class deployment:
- parameterize image/port/model source/shm/log level/cache dirs via .env
- add /status healthcheck and unless-stopped restart policy
- persist caches in named volumes by default, overridable to host paths
- drop the deprecated 'version' field

Add an 'offline' compose profile that starts a private PyPI server
(pypiserver backed by ./wheels) for air-gapped hosts. The offline config
routes all runtime virtual-env installs to the private index through two
complementary channels: a mounted pip.conf consumed by the pip-config
inheritance (which also blocks the hardcoded vLLM/SGLang public extra
indexes), and UV_* env vars covering uv invocations without index flags.

Add docker-compose.cpu.yml override for CPU-only hosts, offline.env and
.env templates, official documentation page (with zh_CN translation) and
toctree entry.
@XprobeBot XprobeBot added this to the v2.x milestone Jul 13, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust Docker Compose deployment setup for Xinference, supporting both online and offline/air-gapped environments. It includes a private PyPI server configuration to serve local Python wheels, a CPU-only override file, environment templates, and comprehensive documentation in English and Chinese. The review feedback highlights two issues in the offline configuration: the pypiserver healthcheck endpoint in docker-compose.yml needs to be corrected to ~health instead of /health to prevent 404 errors, and the UV_INSECURE_HOST variable in offline.env.example should specify only the hostname without the port to ensure proper insecure connection handling by uv.

Comment thread xinference/deploy/docker/docker-compose.yml
Comment thread xinference/deploy/docker/offline.env.example
…erver on Linux

The pypiserver image entrypoint refuses to start unless its internal user
(UID 9898) has read/write/execute permission bits on /data/packages, even
though the compose file mounts the wheels directory read-only. Docker
Desktop on macOS masks this, but on Linux hosts the offline profile
crash-loops without a chmod. Found during verification on a real GPU host.
@OliverBryant

Copy link
Copy Markdown
Collaborator Author

GPU verification results (real hardware)

Completed end-to-end verification on a Linux x86_64 host with 2× RTX 3090 Ti (driver 580, Docker 29.6.1, Compose v5.3.1), using the official xprobe/xinference:latest image.

Online GPU deployment

  • docker compose up -d → healthy in 40s, GPU passthrough working.
  • Launched Qwen3-Instruct 4B with the vLLM engine (--gpu-idx 1 --max_model_len 32768): runtime virtual-env created, vllm>=0.11.2 satisfied by the image (skip-installed), model loaded with ~22 GB VRAM, chat completions served.
  • Captured the engine-default index injection in online mode: settings(extra_index_url=['https://wheels.vllm.ai/0.19.0/cu130', 'https://download.pytorch.org/whl/cu130'], index_strategy=unsafe-best-match) — matching the behavior described in the PR body.

Offline profile on GPU

  • Same model/engine relaunched with --profile offline after wiping the venv: the install settings became settings(index_url=http://xinference-pypiserver:8080/simple, extra_index_url=http://xinference-pypiserver:8080/simple, trusted_host=xinference-pypiserver)the hardcoded public vLLM/PyTorch indexes are fully overridden by the private index, and inference on GPU works with HF_HUB_OFFLINE=1.
  • Host port collision handled by .env overrides (XINFERENCE_PYPISERVER_PORT), as designed.

Issue found & fixed (commit 4601926): on Linux hosts the pypiserver container crash-loops unless the mounted wheels directory grants rwx permission bits to its internal user (UID 9898) — its entrypoint enforces this even though the volume is mounted read-only. Docker Desktop on macOS masks the problem. Documented chmod -R a+rwX ./wheels in the offline guide and in a compose file comment.

Unrelated observation for a potential follow-up issue: when a vLLM engine fails during load (e.g. insufficient free VRAM), xinference launch still exits 0 and prints the model uid while the supervisor rolls the model back — the CLI should propagate the failure.

The offline profile redirects all downloads to local sources but does not
by itself prevent containers from reaching the Internet when the host has
connectivity. docker-compose.airgap.yml moves xinference and the private
PyPI server onto an internal Docker network with no external routing, and
adds a minimal socat TCP gateway to keep the API port published on the
host (Docker does not publish ports of internal-only networks). The
gateway only relays inbound traffic and cannot serve as an egress path.

Verified on a Linux GPU host: external requests (pypi.org, pytorch.org,
raw TCP to 8.8.8.8) are blocked from inside the container while the
private index stays reachable, and vLLM inference through the gateway
works end to end.
@OliverBryant

Copy link
Copy Markdown
Collaborator Author

Added: enforced network isolation (air-gap override)

The offline profile alone redirects downloads to local sources but does not prevent egress when the host happens to have connectivity. Commit 176dc3a adds docker-compose.airgap.yml, which guarantees isolation at the network level:

  • xinference and xinference-pypiserver move onto an internal: true Docker network (no external routing, no NAT).
  • Since Docker does not publish ports of internal-only networks, a minimal alpine/socat gateway spans the default and isolated networks and forwards the API port. It only relays inbound traffic to xinference:9997 — the containers cannot use it as an egress path.
docker compose --profile offline -f docker-compose.yml -f docker-compose.airgap.yml up -d

Verified on the Linux GPU host, from inside the xinference container:

Check Result
https://pypi.org blocked (no route)
http://download.pytorch.org blocked
raw TCP to 8.8.8.8:53 (bypasses DNS/HTTP) blocked
http://xinference-pypiserver:8080/health reachable
host → API via gateway (/status, chat completions with vLLM on GPU) working

Docs updated with an "Enforced network isolation (optional)" subsection (incl. zh_CN translation) covering usage, the extra alpine/socat image to transfer, and how to verify the isolation.

@qinxuye qinxuye left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One blocking issue remains in the true offline installation path.

Comment thread xinference/deploy/docker/pip.conf
…nfigured install paths

When the package list carries a #system_torch#-style marker and the
installed torch has a CUDA suffix, _prepare_virtual_env force-prepended
https://download.pytorch.org/whl/cu* to extra_index_url even when an
extra index had been configured explicitly via the model spec or
inherited pip config. uv treats an unreachable extra index as fatal
rather than falling back, so in air-gapped deployments model launches
failed even with all wheels present on the private index.

Only inject the auto-detected CUDA wheel URL when the extra index is
unset or was filled in by engine defaults; an explicitly configured
index now stays authoritative. Default online behavior is unchanged.

Add regression tests covering both the suppression and the default
injection paths for #system_torch#.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants