-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile
More file actions
155 lines (141 loc) · 8.22 KB
/
Copy pathDockerfile
File metadata and controls
155 lines (141 loc) · 8.22 KB
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# ASAREE's application image.
#
# uv is used to install (it's the only thing that understands
# [tool.uv.sources] -- Motoro is a pinned git dependency and
# asaree-workspace-core an editable local path, neither of which a bare
# `pip install .` can resolve), but the app itself is launched with the
# venv's own uvicorn directly -- not `uv run` -- so a running container never
# depends on uv being invoked per request.
#
# uv stays in the final image deliberately (unlike a slimmed multi-stage
# build): the app spawns its bundled MCP servers (asaree-workspace,
# motoro-okf, and the six asaree-sklearn-* servers -- see
# asaree.services.system_mcp_servers) via `uv run --directory <repo root>
# python -m ...`, the same subprocess convention used in every dev environment
# so far. Removing uv here would just move the same "no uv at runtime" problem
# one level down.
FROM python:3.13-slim AS application
WORKDIR /app
# git: needed by uv to resolve Motoro's pinned git dependency below.
RUN apt-get update && apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir uv
# README.md is not documentation here: pyproject declares `readme =
# "README.md"`, and hatchling refuses to build the wheel without it.
COPY pyproject.toml uv.lock README.md ./
# Editable local path dependencies -- must be present before `uv sync`, not
# copied in after: asaree-workspace-core, plus the seven mcp-servers/ packages
# (the six bundled domain MCP servers and the asaree-sklearn-core library they
# wrap). Installing them here is what lets each server's registered command be
# `uv run --directory /app python -m <module>` with no host path in it, so a
# fresh deployment auto-registers them with no bind mount and no
# register_servers.py step.
COPY workspace-core/ ./workspace-core/
COPY mcp-servers/ ./mcp-servers/
# Dependencies only, deliberately BEFORE `COPY src/` below -- src/ changes on
# nearly every commit, and this project's own `asaree` package doesn't need
# to be installed yet to resolve/build every third-party + Motoro
# dependency. Splitting this out means an ordinary code change reuses this
# entire layer from cache instead of re-fetching pandas/scipy/sklearn/
# pyarrow/Motoro (a multi-GB re-download+rebuild) on every rebuild --
# without this split, that used to happen on every single build and left a
# fresh several-GB orphaned layer in the build cache each time, since nothing
# prunes superseded layers automatically.
#
# Motoro is a pinned git dependency (see pyproject.toml). While that repo is
# private, uv needs a credential to fetch it that the host's `gh`-backed git
# credential helper doesn't carry into an isolated build. Passed as a
# BuildKit secret (compose.yml's `secrets:`), injected via one-shot
# git config env vars so the token touches only this RUN's environment --
# never a file, never a committed layer.
#
# The secret is optional, and this layer needs no change once Motoro goes
# public: an unset GH_TOKEN just makes the rewrite below expand to
# `https://oauth2:@github.com/`, and GitHub ignores an empty password on a
# public repo and serves it anonymously. So there is nothing here to remember
# to undo -- only the `GH_TOKEN=...` prefix on the build command becomes
# unnecessary.
#
# The cache mount (uv's own download/wheel cache) is separate from this
# layer's own history -- it's updated in place across builds rather than
# re-snapshotted every time, so it doesn't grow the image or the build cache
# the way baking the same downloads into a plain RUN layer repeatedly would.
# It also means a genuine dependency bump (the one case this layer really
# does need to rerun) reuses whatever's already downloaded instead of
# refetching everything from scratch.
RUN --mount=type=secret,id=gh_token \
--mount=type=cache,target=/root/.cache/uv,sharing=locked \
GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0="url.https://oauth2:$(cat /run/secrets/gh_token)@github.com/.insteadOf" \
GIT_CONFIG_VALUE_0="https://github.com/" \
uv sync --frozen --no-dev --no-install-project
COPY src/ ./src/
# One-off operational scripts (e.g. promote_score_metrics.py) -- run via
# `docker exec asaree-app python scripts/<name>.py ...`, not imported by the
# app itself, but still needs to actually be in the image to be runnable.
COPY scripts/ ./scripts/
# Fast: every dependency is already installed above from cache; this just
# links the local `asaree` package itself (editable install -- reads src/
# directly at import time, so this step never needs to rerun the heavy
# dependency resolution/download above just because app code changed).
#
# The .git mount is what lets this step build at all: pyproject.toml has no
# literal version, hatch-vcs derives it from the repo's tags, and it needs the
# git metadata to do that.
#
# `from=gitdir` -- a *named* build context (compose.yml's `additional_contexts`)
# rather than the main one -- for two reasons. It never lands in an image layer,
# and .dockerignore can go on excluding .git/ from the main context, so no future
# `COPY . .` added here can bake the history into an image. Building this by hand
# outside Compose therefore needs the context passed explicitly:
#
# docker build --build-context gitdir=./.git --secret id=gh_token,env=GH_TOKEN .
#
# The `update-index` line is what keeps the derived version honest. /app is a
# deliberately partial checkout -- frontend/, tests/, docs/ and the repo root's
# own files are never COPYed -- so against the mounted index every one of them
# reads as deleted and `git describe --dirty` (which is exactly what hatch-vcs
# runs) appends `-dirty`. setuptools_scm treats dirty like distance: a build of
# the tagged commit v0.6.0 came out as 0.6.1.dev0, i.e. every release build
# announced itself as a prerelease of the *next* one. Marking the absent paths
# assume-unchanged makes describe see the clean tree the tag actually names.
# The cost, accepted: uncommitted edits to the files that ARE copied no longer
# mark the build dirty either -- "which release is this server" is what the
# badge is for, and a dev build is identified by its commit distance anyway.
# `readwrite` is required to write the index; the mount is a throwaway copy of
# the named context, so the host's .git is not touched.
RUN --mount=type=bind,from=gitdir,target=/app/.git,readwrite \
--mount=type=cache,target=/root/.cache/uv,sharing=locked \
git -C /app ls-files -d -z | xargs -0 -r git -C /app update-index --assume-unchanged \
&& uv sync --frozen --no-dev
# Absent on purpose: the repo's .env. AsareeSettings reads host-side URLs
# (localhost:5432) that are wrong inside a compose network; real values
# arrive as environment variables at `docker run`/`docker compose` time
# instead. .dockerignore keeps .env out of the build context so it can't be
# copied in by accident.
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
CMD ["uvicorn", "asaree.app:app", "--host", "0.0.0.0", "--port", "8000"]
# Opt-in test image used by the asaree-tests Compose service. Keep pytest and
# the test tree out of the production image while reusing every application
# dependency layer above. The git context is required for the same hatch-vcs
# reason as the application install.
FROM application AS test
COPY tests/ ./tests/
# One backend contract test compares the mirrored TypeScript catalog with the
# Python source of truth. Keep the test image narrow while making that source
# available at the same repository-relative path used outside containers.
COPY frontend/src/lib/metricCatalog.ts ./frontend/src/lib/metricCatalog.ts
# Same assume-unchanged guard as the application stage: this reinstalls the
# project (now with tests/ present, so a different set of paths is missing),
# which regenerates _version.py -- without it the test image would overwrite
# the correct version with a dirty one.
RUN --mount=type=bind,from=gitdir,target=/app/.git,readwrite \
--mount=type=cache,target=/root/.cache/uv,sharing=locked \
git -C /app ls-files -d -z | xargs -0 -r git -C /app update-index --assume-unchanged \
&& uv sync --frozen --group dev
CMD ["pytest", "tests/", "-q", "--tb=short"]
# Keep the default build target as the production application. Merely adding
# the test stage must not put test-only dependencies or source in deployed
# images built without an explicit target.
FROM application AS runtime