File size: 4,611 Bytes
c14ceee 051f280 c14ceee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | # AIOS web β the React shell + FastAPI over the platform data layer. THE CANONICAL IMAGE:
# this is the one that actually runs, and the one a host migration re-points (C1e).
#
# β WHY A SECOND DOCKERFILE EXISTS (ops/Dockerfile.api), stated here so nobody "tidies" the wrong
# one away. It is NOT a duplicated service β it is the SAME service built from a DIFFERENT BUILD
# CONTEXT:
# * HF Spaces: the context is the Space repo root, which `deploy_web.py` uploads FLATTENED β
# `api/`, `web/`, `platform/`, `requirements.txt` all at top level. Hence the paths here.
# * ops/compose.yml: the context is the local repo root, where the same files live at
# `aios-web/api/`, `aios-web/web/`, `platform/`. Different prefixes, same content.
# β THE REAL FIX, and it is a follow-up rather than something to do the day of a first deploy:
# make `deploy_web.py` upload with the REPO layout so both contexts are identical, then this file
# serves both and `ops/Dockerfile.api` is deleted. Until then `ops/verify_portability.py` gates
# the two against each other on every property that matters (same app object, same launcher
# contract, non-root, healthchecked, no baked secrets), so they cannot drift silently.
#
# Two stages: node builds the React bundle, python serves it + the JSON API. Login is the app's own
# branded page over a signed session cookie (wave 1 removed the HTTP Basic gate).
# ---- stage 1: build the React bundle ----
FROM node:22-slim AS web
WORKDIR /web
# Resolve deps FRESH from the public registry (the local lockfile pinned a mirror-only lodash
# 4.18.1 that isn't on npmjs.org; a fresh install gets the public 4.17.x that glide accepts).
COPY web/package.json ./
RUN npm install --no-audit --no-fund --no-package-lock
COPY web/ ./
RUN rm -f package-lock.json && npm run build # -> /web/dist
# ---- stage 2: python API serving the bundle ----
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY api/ ./api/
# the reused data layer (modules/core/harness)
# β COMMENT ON ITS OWN LINE. Docker does NOT support a trailing comment on an instruction:
# it parsed `# the reused data layer ...` as extra SOURCE PATHS and the first build ever run
# died with `failed to calculate checksum ... "/the": not found`. Only RUN tolerates it, and
# only because the shell eats the `#`. Gated in ops/verify_portability.py.
COPY platform/ ./platform/
# the ephemeral-HF-Job scripts the API submits BY PATH (wave 31, R10/C5)
# β WITHOUT THIS LINE THE DEPLOY UPLOADS jobs/ AND THE IMAGE STILL DOES NOT HAVE IT. Found at
# wave 31 close: `deploy_web.py::_jobs_files()` was fixed to SHIP the two files, `--check` proved
# `jobs=2`, and the container would still have answered "the browser job's script is missing from
# this deployment" β because reaching the Space REPO and reaching the IMAGE are two different
# things, and only this instruction does the second. Same shape as the manifest defect one layer
# up: a file the product opens BY PATH is invisible to every import-following guard we own.
COPY jobs/ ./jobs/
COPY --from=web /web/dist ./web/dist
# Run unprivileged β the C2 security baseline, and matched to ops/Dockerfile.api so the two images
# do not differ on a property that would only surface after a migration. uid 1000 is what HF
# Spaces expects.
RUN useradd --create-home --uid 1000 user && chown -R user:user /app
USER user
ENV RI_DIR=/app/platform \
PORT=7860 \
PYTHONUNBUFFERED=1 \
AIOS_PREWARM=1
# β AIOS_INSECURE_SSL is deliberately NOT set. It disables TLS verification for every outbound
# call in the process and exists for one developer laptop. ops/verify_portability.py fails if any
# deploy config sets it.
EXPOSE 7860
# Liveness via stdlib rather than curl β it keeps the image free of an apt layer that exists only
# for the healthcheck.
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD python -c "import os,urllib.request,sys; sys.exit(0 if urllib.request.urlopen(f'http://127.0.0.1:{os.environ.get(\"PORT\",\"7860\")}/api/health',timeout=4).status==200 else 1)"
# β THE PORT COMES FROM THE ENVIRONMENT, ALWAYS. The application itself reads PORT nowhere β it is
# a launcher argument β which is what makes moving hosts a config change instead of a code change.
# Secrets (Odoo creds, APP_PASSWORD, AIOS_SESSION_SECRET, APP_BASE_URL) arrive as env at RUN time
# and are never baked; the repo-root .dockerignore drops **/.env from the build context.
CMD ["sh", "-c", "uvicorn api.main:app --host 0.0.0.0 --port ${PORT}"]
|