391 lines
20 KiB
Docker
391 lines
20 KiB
Docker
# Omnigent images: server (default target) + host (`--target host`).
|
|
#
|
|
# The default/final target builds the server image (external-runner
|
|
# mode): the FastAPI / WebSocket coordinator — HTTP + SSE routes, the
|
|
# runner WebSocket tunnel acceptor, and the SQLAlchemy stores. It does
|
|
# NOT execute agent harnesses — runners run on the user's local machine
|
|
# and dial in via WS /v1/runner/tunnel.
|
|
#
|
|
# As a consequence: no tmux, no git, no harness SDK runtime
|
|
# requirements in the final image. The omnigent package is still
|
|
# installed in full (its pyproject pulls claude-agent-sdk +
|
|
# openai-agents transitively), so the bits are on disk; they are
|
|
# simply never imported because HarnessProcessManager is never
|
|
# invoked in this deployment mode.
|
|
#
|
|
# The `host` target is the inverse: a prebaked Omnigent HOST image for
|
|
# remote sandboxes (e.g. `omnigent sandbox create --provider modal`)
|
|
# and server-launched managed hosts. It bakes the full omnigent
|
|
# install plus the tools a host needs at runtime — git (workspaces /
|
|
# worktrees), tmux (terminal sessions spawned by native harnesses),
|
|
# and the coding-harness CLIs (claude / codex / pi, via Node; kiro-cli via
|
|
# Kiro's installer) — and
|
|
# skips everything server-only: no SPA bundle, no psycopg, no
|
|
# uvicorn entrypoint. Modal's `Image.from_registry` requirements shape
|
|
# it: `python` + `pip` on $PATH, and CMD-only (an ENTRYPOINT that
|
|
# doesn't exec its args would block Modal's own runtime from running).
|
|
#
|
|
# Both images publish multi-arch (linux/amd64 + linux/arm64) — see
|
|
# .github/workflows/oss-publish-images.yml — so they run natively on
|
|
# arm64 hosts (Apple Silicon laptops, arm64 clusters) as well as amd64.
|
|
# Nothing below is arch-specific: the python/node base images are
|
|
# multi-arch and apt/pip/npm/COPY-from-node all resolve per-arch under
|
|
# buildx. Amd64-only consumers (Modal, Daytona, CoreWeave) keep pulling
|
|
# the amd64 variant from the manifest list, unchanged.
|
|
#
|
|
# Build (from repo root):
|
|
#
|
|
# docker build -t omnigent-server:latest \
|
|
# -f deploy/docker/Dockerfile .
|
|
#
|
|
# docker build -t omnigent-host:latest --target host \
|
|
# -f deploy/docker/Dockerfile .
|
|
#
|
|
# Behind corporate package proxies (the host target also installs the
|
|
# harness CLIs from npm, so it needs the npm proxy too):
|
|
#
|
|
# docker build -t omnigent-server:latest \
|
|
# -f deploy/docker/Dockerfile \
|
|
# --build-arg PYPI_INDEX_URL=https://pypi-proxy.example.com/simple \
|
|
# --build-arg NPM_CONFIG_REGISTRY=https://npm-proxy.example.com .
|
|
#
|
|
# Or just use the bundled compose file (Postgres included):
|
|
#
|
|
# cd deploy/docker && docker compose up -d
|
|
|
|
# Must satisfy pyproject requires-python (>=3.12); 3.11 fails dependency resolution.
|
|
ARG PYTHON_VERSION=3.12
|
|
ARG NODE_VERSION=20
|
|
|
|
# ── Web UI builder ──────────────────────────────────────
|
|
# Builds the web SPA so `docker build` works from a clean checkout —
|
|
# no separate `cd web && npm run build` step, no "SPA bundle missing"
|
|
# hard-fail. vite.config emits to ../omnigent/server/static/web-ui
|
|
# (relative to web/), so from /web/web the bundle lands at
|
|
# /web/omnigent/server/static/web-ui, which the server builder overlays.
|
|
# Server-only: the host target never reaches this stage.
|
|
#
|
|
# Behind a corporate npm proxy, pass it through:
|
|
# --build-arg NPM_CONFIG_REGISTRY=https://npm-proxy.example.com
|
|
FROM node:${NODE_VERSION}-slim AS web-builder
|
|
ARG NPM_CONFIG_REGISTRY=
|
|
ENV NPM_CONFIG_REGISTRY=${NPM_CONFIG_REGISTRY}
|
|
WORKDIR /web/web
|
|
# Manifests first so the install layer caches across pure source edits.
|
|
COPY web/package.json web/package-lock.json ./
|
|
RUN npm install --no-audit --no-fund
|
|
COPY web/ ./
|
|
RUN npm run build
|
|
|
|
# ── Python builder (shared: server + host) ──────────────
|
|
# Installs the package (and its transitive native-extension deps) into
|
|
# a virtualenv the runtime stages copy verbatim. Keeps build-essential
|
|
# out of the shipped images. Deliberately SPA-free and psycopg-free so
|
|
# the host target can build without node; the server-only additions
|
|
# live in server-builder below.
|
|
FROM python:${PYTHON_VERSION}-slim AS builder
|
|
|
|
ARG PYPI_INDEX_URL=https://pypi.org/simple
|
|
|
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
|
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends build-essential \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
RUN pip install --index-url ${PYPI_INDEX_URL} --no-cache-dir uv
|
|
|
|
WORKDIR /build
|
|
|
|
# Manifests + SDK path-deps first so the install layer caches across
|
|
# pure source edits.
|
|
COPY pyproject.toml setup.py ./
|
|
# License + third-party attribution travel into the published images (the host
|
|
# and runtime targets COPY /build below), since the image redistributes the
|
|
# dependency bytes baked in by the install step.
|
|
COPY LICENSE NOTICE ./
|
|
COPY sdks/ ./sdks/
|
|
COPY omnigent/ ./omnigent/
|
|
# The built-in polly agent's packaged bundle (omnigent/resources/examples/
|
|
# polly) is a symlink into the top-level examples/ tree. Ship examples/ so the
|
|
# symlink resolves at boot; without it the new-session picker silently drops
|
|
# polly (the seeder skips it when its bundle is absent). ~120K of YAML.
|
|
COPY examples/ ./examples/
|
|
|
|
# Standalone venv we can copy out as a single directory. VIRTUAL_ENV
|
|
# tells uv to install into this venv (not the slim image's system
|
|
# Python) — without it, uv falls back to system Python and the
|
|
# copied /opt/venv lands in the runtime stage empty.
|
|
RUN python -m venv /opt/venv
|
|
ENV VIRTUAL_ENV=/opt/venv \
|
|
PATH="/opt/venv/bin:${PATH}"
|
|
|
|
# Install omnigent. Editable install (-e) is required because
|
|
# pyproject.toml declares the sibling SDKs as editable path deps via
|
|
# [tool.uv.sources]. The runtime stages preserve /build/ so the venv's
|
|
# .pth references stay valid.
|
|
RUN uv pip install --no-cache-dir --index-url ${PYPI_INDEX_URL} -e .
|
|
|
|
# ── Server builder ──────────────────────────────────────
|
|
# Server-only additions on top of the shared builder: the SPA bundle
|
|
# and the Postgres driver.
|
|
FROM builder AS server-builder
|
|
|
|
ARG PYPI_INDEX_URL=https://pypi.org/simple
|
|
|
|
# Overlay the SPA bundle built in the web-builder stage, so a clean
|
|
# checkout (with no prebuilt bundle on the host) still produces a
|
|
# complete image. This replaces the old "prebuild or hard-fail" check.
|
|
COPY --from=web-builder /web/omnigent/server/static/web-ui ./omnigent/server/static/web-ui
|
|
RUN test -f ./omnigent/server/static/web-ui/index.html \
|
|
|| (echo "ERROR: SPA bundle missing after web-builder stage — check the web build." && exit 1)
|
|
|
|
# psycopg[binary] is not a baseline dep — pulled in by the
|
|
# [databricks] extra in pyproject — so add it explicitly here.
|
|
RUN uv pip install --no-cache-dir --index-url ${PYPI_INDEX_URL} 'psycopg[binary]>=3.1,<4'
|
|
|
|
# Optional managed-sandbox provider extras for the SERVER (the launcher imports
|
|
# the provider SDK — e.g. the kubernetes client for `sandbox.provider:
|
|
# kubernetes`). Off by default; the runner host image needs none of these. Build
|
|
# with `--build-arg OMNIGENT_EXTRAS=kubernetes` (comma-separate for several).
|
|
ARG OMNIGENT_EXTRAS=
|
|
RUN if [ -n "${OMNIGENT_EXTRAS}" ]; then \
|
|
uv pip install --no-cache-dir --index-url ${PYPI_INDEX_URL} -e ".[${OMNIGENT_EXTRAS}]"; \
|
|
fi
|
|
|
|
# ── Node alias stage ─────────────────────────────────────
|
|
# Pure alias so the host stage can COPY node out of a version-pinned
|
|
# image; contributes no layers of its own.
|
|
FROM node:${NODE_VERSION}-slim AS node-runtime
|
|
|
|
# ── Host runtime (`--target host`) ──────────────────────
|
|
# Prebaked Omnigent host for remote sandboxes: full omnigent install,
|
|
# git + tmux for harness runtime needs, procps + lsof so the antigravity-native
|
|
# executor can discover agy's connect-RPC port (antigravity_native_rpc.py uses
|
|
# `pgrep` in _list_agy_pids to find agy processes — with a /proc-scan fallback if
|
|
# pgrep is absent — then `lsof` in resolve_language_server_port to read their
|
|
# loopback ports; lsof has NO fallback, so a missing lsof silently yields "no
|
|
# port" and breaks web-turn injection), bubblewrap to OS-sandbox the
|
|
# native harness terminals (mandatory and fail-loud on Linux), curl + CA
|
|
# certificates for outbound HTTPS and in-sandbox downloads, plus the
|
|
# coding-harness CLIs (claude / codex / pi / kiro-cli) so claude-sdk,
|
|
# claude-native, codex, pi, and kiro-native agents can run in managed
|
|
# sandboxes without an in-sandbox install. No SPA, no psycopg, no server
|
|
# entrypoint.
|
|
#
|
|
# Also carries two additions required only by the NVIDIA OpenShell provider
|
|
# (deploy/openshell/README.md) and inert for the root-based providers
|
|
# (Modal / Daytona / CoreWeave):
|
|
# - iproute2 / nftables: OpenShell puts each sandbox in its own network
|
|
# namespace and routes egress through a policy proxy; the supervisor
|
|
# shells out to `ip` to build that netns and refuses to start a sandbox
|
|
# without it. The other providers create no namespaces, so it sits unused.
|
|
# - a non-root `sandbox` user/group: OpenShell drops privileges to a user
|
|
# literally named `sandbox` before running the agent (defense in depth)
|
|
# and fails closed if it is absent. The other providers run as root and
|
|
# never reference it.
|
|
FROM python:${PYTHON_VERSION}-slim AS host
|
|
ARG NPM_CONFIG_REGISTRY=
|
|
ENV NPM_CONFIG_REGISTRY=${NPM_CONFIG_REGISTRY}
|
|
|
|
# IS_SANDBOX: devcontainer-convention flag consulted by Claude Code —
|
|
# it refuses --dangerously-skip-permissions under root without it, and
|
|
# sandbox containers run as root. The host forwards it to runners (see
|
|
# _RUNNER_ENV_ALLOWLIST in omnigent/host/connect.py) so the
|
|
# claude-sdk harness can start. Only this image sets it; laptops never
|
|
# have it.
|
|
ENV PYTHONUNBUFFERED=1 \
|
|
PYTHONDONTWRITEBYTECODE=1 \
|
|
PATH="/opt/venv/bin:${PATH}" \
|
|
IS_SANDBOX=1
|
|
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends \
|
|
git tmux procps lsof bubblewrap curl ca-certificates unzip \
|
|
iproute2 nftables \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# OpenShell-required non-root user (see the header note). UID/GID in the high
|
|
# range per OpenShell's bring-your-own-container guidance, so that without
|
|
# user-namespace remapping the sandbox user maps to an unprivileged, unused
|
|
# host id. Unused by the root-based providers.
|
|
RUN groupadd -g 1000660000 sandbox \
|
|
&& useradd -m -d /sandbox -u 1000660000 -g sandbox sandbox
|
|
|
|
# Git credential helper for private repositories over HTTPS: answers
|
|
# `git credential get` from GIT_TOKEN / GIT_USERNAME in the
|
|
# environment (injected via Modal secrets — see deploy/modal/README.md
|
|
# "Git credentials"), so the managed launch's repository clone AND the
|
|
# agent's later fetch/push authenticate without writing credentials to
|
|
# disk. Emits nothing when GIT_TOKEN is unset, leaving anonymous
|
|
# clones of public repositories untouched. GIT_USERNAME defaults to
|
|
# x-access-token (GitHub's token-auth username; GitLab users set
|
|
# GIT_USERNAME=oauth2). --system so it applies to any sandbox user.
|
|
RUN git config --system credential.helper \
|
|
'!f() { [ "$1" = get ] || return 0; [ -n "$GIT_TOKEN" ] || return 0; printf "username=%s\npassword=%s\n" "${GIT_USERNAME:-x-access-token}" "$GIT_TOKEN"; }; f'
|
|
|
|
# Node runtime for the harness CLIs, copied from the official image
|
|
# (same Debian base as python-slim, so the binary is ABI-compatible)
|
|
# rather than apt — keeps the version pinned to NODE_VERSION and skips
|
|
# nodesource setup. npm/npx are symlinks into npm's node_modules.
|
|
# (`node-runtime` is the alias stage below — COPY --from does not
|
|
# expand build args, so the FROM line does the ${NODE_VERSION} part.)
|
|
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 ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
|
&& ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|
|
|
|
# The harness CLI set mirrors omnigent/onboarding/harness_install.py
|
|
# (the binary/package map `omnigent setup` installs from) — keep the
|
|
# two in sync. Unpinned on purpose: the official image is rebuilt by
|
|
# CI, so it tracks the same "latest" a laptop install would get.
|
|
RUN npm install -g --no-audit --no-fund \
|
|
@anthropic-ai/claude-code \
|
|
@openai/codex \
|
|
@earendil-works/pi-coding-agent \
|
|
&& npm cache clean --force
|
|
|
|
# Kiro CLI is not published as an npm package, and its installer has NO version
|
|
# flag — `curl …/install | bash` always fetches `latest`, so the image was
|
|
# non-deterministic. The kiro-native harness is behaviorally coupled to a
|
|
# specific kiro-cli build (Escape-interrupt leaves an empty composer, the
|
|
# bracketed-paste multi-line path, the session-JSONL layout — all verified
|
|
# against 2.10.0; grep `kiro-cli 2.10.0`). So pin it the same way as `agy` below:
|
|
# fetch the immutable per-arch zip from the versioned CDN path and verify its
|
|
# sha256, run the package's own (network-free) install.sh, then copy the binaries
|
|
# onto a system PATH dir every sandbox user shares. A trailing `kiro-cli
|
|
# --version` check asserts the unpacked binary really is the pinned version — a
|
|
# cheap sanity guard atop the sha256. To adopt a new kiro-cli: re-verify the
|
|
# coupled behavior, then bump KIRO_CLI_VERSION + both
|
|
# SHA256s (the `sha256` fields in
|
|
# https://prod.download.cli.kiro.dev/stable/latest/manifest.json). Keep in sync
|
|
# with deploy/docker/Dockerfile.ubi.
|
|
ARG KIRO_CLI_VERSION=2.10.0
|
|
ARG KIRO_CLI_SHA256_AMD64=be9d8b6d7c44f93a83ca22466043d98ad058e6ed3c12fffd068f3fb8a60b3b70
|
|
ARG KIRO_CLI_SHA256_ARM64=0afb37399b9e2847c2f2e3f5d9052c8bc52bbf1e30401ea284a602661bce34bc
|
|
RUN set -eu; \
|
|
case "$(uname -m)" in \
|
|
x86_64) asset="kirocli-x86_64-linux.zip"; sha="$KIRO_CLI_SHA256_AMD64" ;; \
|
|
aarch64) asset="kirocli-aarch64-linux.zip"; sha="$KIRO_CLI_SHA256_ARM64" ;; \
|
|
*) echo "ERROR: unsupported arch '$(uname -m)' for kiro-cli" >&2; exit 1 ;; \
|
|
esac; \
|
|
curl -fsSL -o /tmp/kiro.zip "https://prod.download.cli.kiro.dev/stable/${KIRO_CLI_VERSION}/${asset}"; \
|
|
echo "${sha} /tmp/kiro.zip" | sha256sum -c -; \
|
|
unzip -q /tmp/kiro.zip -d /tmp/kiro; \
|
|
KIRO_CLI_SKIP_SETUP=1 sh /tmp/kiro/kirocli/install.sh; \
|
|
install -m 0755 /root/.local/bin/kiro-cli /usr/local/bin/kiro-cli; \
|
|
if [ -f /root/.local/bin/kiro-cli-chat ]; then \
|
|
install -m 0755 /root/.local/bin/kiro-cli-chat /usr/local/bin/kiro-cli-chat; \
|
|
fi; \
|
|
rm -rf /tmp/kiro /tmp/kiro.zip; \
|
|
installed="$(/usr/local/bin/kiro-cli --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)"; \
|
|
[ "$installed" = "$KIRO_CLI_VERSION" ] || { \
|
|
echo "ERROR: kiro-cli reports '${installed:-<none>}', expected '$KIRO_CLI_VERSION'." >&2; exit 1; }; \
|
|
echo "kiro-cli ${KIRO_CLI_VERSION} pinned (sha256 verified)"
|
|
# Antigravity CLI (`agy`) — the antigravity-native harness shells out to `agy`
|
|
# on the host, launching it in a tmux pane (see omnigent/antigravity_native*.py),
|
|
# so a managed host image must carry it. It is NOT an npm package
|
|
# (harness_install.py lists agy as a non-npm, installer-script harness), so it
|
|
# can't join the `npm install -g` set above. The tarball holds a single
|
|
# self-contained ``antigravity`` binary; install it as ``agy`` on a system PATH
|
|
# dir every user shares (its bootstrapper default ~/.local/bin is per-user and
|
|
# off the venv PATH). ``test -x`` fails the build loudly if the layout changes.
|
|
#
|
|
# Version + integrity pin: the native harness is behaviorally coupled to a
|
|
# specific agy build (out-of-order transcript writes, connect-RPC quirks, and TUI
|
|
# injection are all verified against 1.0.10 — grep ``agy 1.0.10`` under
|
|
# omnigent/antigravity_native*). The official ``install.sh`` bootstrapper has NO
|
|
# version flag — it always fetches the LATEST build from an auto-updater manifest
|
|
# and old builds are not retained at any stable, reconstructable URL — so it
|
|
# cannot pin anything. Instead we fetch the exact, immutable per-arch release
|
|
# asset from GitHub and verify its SHA256: this both holds the verified version
|
|
# AND fails the build if the bytes ever change underneath us, which is the actual
|
|
# supply-chain control (a version-string check alone is not). To adopt a new agy:
|
|
# re-verify the coupled behavior, then bump AGY_VERSION and both SHA256s (from
|
|
# https://github.com/google-antigravity/antigravity-cli/releases).
|
|
ARG AGY_VERSION=1.0.10
|
|
ARG AGY_SHA256_AMD64=6547cf9a37227f26004fa4b805418b1df96f54c57b9723ca7d10864d2610bb0f
|
|
ARG AGY_SHA256_ARM64=4674fabc3681221e54c90d15077c9a97a25ea71222001dabe44bf1576e888593
|
|
RUN set -eu; \
|
|
arch="$(dpkg --print-architecture)"; \
|
|
case "$arch" in \
|
|
amd64) asset="agy_cli_linux_x64.tar.gz"; sha="$AGY_SHA256_AMD64" ;; \
|
|
arm64) asset="agy_cli_linux_arm64.tar.gz"; sha="$AGY_SHA256_ARM64" ;; \
|
|
*) echo "ERROR: unsupported arch '$arch' for agy" >&2; exit 1 ;; \
|
|
esac; \
|
|
url="https://github.com/google-antigravity/antigravity-cli/releases/download/${AGY_VERSION}/${asset}"; \
|
|
curl -fsSL -o /tmp/agy.tar.gz "$url"; \
|
|
echo "${sha} /tmp/agy.tar.gz" | sha256sum -c -; \
|
|
tar -xzf /tmp/agy.tar.gz -C /tmp antigravity; \
|
|
install -m 0755 /tmp/antigravity /usr/local/bin/agy; \
|
|
rm -f /tmp/agy.tar.gz /tmp/antigravity; \
|
|
test -x /usr/local/bin/agy; \
|
|
installed="$(/usr/local/bin/agy --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)"; \
|
|
if [ "$installed" != "$AGY_VERSION" ]; then \
|
|
echo "ERROR: agy reports '${installed:-<unparseable>}', expected '$AGY_VERSION'." >&2; \
|
|
exit 1; \
|
|
fi; \
|
|
echo "agy ${AGY_VERSION} pinned (sha256 verified)"
|
|
|
|
# Copy the venv and source tree. The editable install's .pth files reference
|
|
# /build/omnigent and /build/sdks/* -- both denied by the k8s Landlock LSM
|
|
# policy. Re-install without -e so the package bytes land in the venv's
|
|
# site-packages and imports no longer require /build at runtime.
|
|
COPY --from=builder /opt/venv /opt/venv
|
|
COPY --from=builder /build /build
|
|
RUN pip install --no-cache-dir /build /build/sdks/python-client /build/sdks/ui \
|
|
&& ! grep -R --include='*.pth' --include='*.egg-link' -nE '/build(/|$)' /opt/venv/lib/python*/site-packages
|
|
|
|
# Sandbox launchers exec commands through `bash -lc`, and Debian's
|
|
# /etc/profile unconditionally resets PATH for login shells — the ENV
|
|
# PATH above would silently drop off and `omnigent` / `pip` would
|
|
# resolve to the system Python instead of the venv. profile.d snippets
|
|
# are sourced after that reset, so this puts the venv back in front.
|
|
RUN echo 'export PATH="/opt/venv/bin:${PATH}"' > /etc/profile.d/omnigent-venv.sh
|
|
|
|
WORKDIR /root
|
|
|
|
# CMD only — Modal's `Image.from_registry` (and the sandbox launchers
|
|
# generally) supply their own entrypoint command; an ENTRYPOINT here
|
|
# would hijack it. The hold-open default mirrors what `omnigent
|
|
# sandbox create` runs so a bare `docker run` behaves the same way.
|
|
CMD ["sleep", "infinity"]
|
|
|
|
# ── Server runtime (default target) ─────────────────────
|
|
FROM python:${PYTHON_VERSION}-slim AS runtime
|
|
|
|
ENV PYTHONUNBUFFERED=1 \
|
|
PYTHONDONTWRITEBYTECODE=1 \
|
|
PATH="/opt/venv/bin:${PATH}"
|
|
|
|
# curl + ca-certificates only — for the HEALTHCHECK and for outbound
|
|
# HTTPS to LLM gateways / external services.
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends curl ca-certificates \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Preserve /build/ in the runtime stage — the venv's editable install
|
|
# .pth files reference /build/omnigent and /build/sdks/* by absolute
|
|
# path. Copying these to /app/ would break the import paths silently.
|
|
COPY --from=server-builder /opt/venv /opt/venv
|
|
COPY --from=server-builder /build /build
|
|
COPY deploy/docker/entrypoint.py /app/entrypoint.py
|
|
|
|
WORKDIR /app
|
|
|
|
# /data is mounted as a persistent volume by docker-compose for the
|
|
# artifact store.
|
|
RUN mkdir -p /data/artifacts
|
|
|
|
ENV PORT=8000 \
|
|
HOST=0.0.0.0 \
|
|
ARTIFACT_DIR=/data/artifacts
|
|
|
|
EXPOSE 8000
|
|
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
|
CMD curl -fsS "http://127.0.0.1:${PORT}/health" || exit 1
|
|
|
|
CMD ["python", "/app/entrypoint.py"]
|