Repository navigation
Expand file tree
/
Copy pathDockerfile
More file actions
119 lines (103 loc) · 5.94 KB
/
Copy pathDockerfile
File metadata and controls
119 lines (103 loc) · 5.94 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
# syntax=docker/dockerfile:1.7
#
# Curule — container image for the multi-project host (default) or a single mesh.
#
# docker build -t curule .
# docker run --rm -p 127.0.0.1:7420:7420 -v mesh-data:/data \
# -e MESH_API_TOKEN="$(openssl rand -hex 32)" -e ANTHROPIC_API_KEY=sk-ant-... curule
#
# What is inside: the compiled runtime, the dashboard, the role prompts and the shipped examples; git
# (agents commit in worktrees); and the Claude Agent SDK's own `claude` executable, installed from npm
# unmodified, because that is how Anthropic's terms require it to run. The image holds no credentials:
# model access is the customer's own ANTHROPIC_API_KEY (or Bedrock / Vertex / Foundry credentials),
# supplied at run time and billed to them by the provider.
#
# Pin the base by digest in your registry's build (`--build-arg NODE_IMAGE=node:22-bookworm-slim@sha256:…`).
ARG NODE_IMAGE=node:22-bookworm-slim
# ---------------------------------------------------------------- build
FROM ${NODE_IMAGE} AS build
WORKDIR /src
# Dependencies first: a source change must not re-download them. The workspace packages' manifests are
# needed for `npm ci` to link them, so `packages/` comes before the install.
COPY package.json package-lock.json ./
COPY packages ./packages
RUN --mount=type=cache,target=/root/.npm npm ci
COPY tsconfig.json THIRD_PARTY_NOTICES.md LICENSE ./
COPY apps ./apps
COPY schemas ./schemas
COPY roles ./roles
COPY examples ./examples
COPY scripts ./scripts
COPY tools ./tools
# Compile, then keep only what runs: no tests, no dev dependencies.
RUN npm run build \
&& rm -rf dist/tests \
&& npm prune --omit=dev
# -------------------------------------------------------------- runtime
FROM ${NODE_IMAGE} AS runtime
# git: agents work in worktrees and commit. tini: the host spawns one process per project and an agent's
# shell spawns more, so PID 1 must reap what its children leave behind. ca-certificates: TLS to the model
# provider. curl is for the health check only. chromium is the crew's eyes: nothing here could render a
# page, so every interface decision was made blind and asserted as text (tools/ux-render.mjs drives it and
# reports measurements the text-only seats can read). fonts-* because a headless render without fonts draws
# boxes, and a font census of boxes is worse than none.
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates tini curl \
fonts-liberation fonts-dejavu-core \
&& rm -rf /var/lib/apt/lists/*
# Nothing here phones home, and the Claude Code binary the agents run does not either: it is told to make no
# nonessential network calls (telemetry, error reports, update checks, feature flags). What leaves the
# container is the agents' calls to the model provider, with the customer's own credentials. To let the binary
# report as it normally would, set CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC to an empty value.
# A fixed, unprivileged identity. Everything that must survive a restart lives under /data: the registry
# and licence (MESH_HOME), each project's event log and workspace (/data/projects, which is also the only
# place the API may register or browse: MESH_PROJECTS_ROOT), and the agents' own CLI sessions (HOME),
# without which a restarted seat cannot resume its conversation.
ARG MESH_UID=10001
RUN useradd --uid ${MESH_UID} --user-group --home-dir /data/user --shell /bin/bash --no-create-home mesh \
&& mkdir -p /data/home /data/user /data/projects \
&& chown -R mesh:mesh /data
ENV NODE_ENV=production \
MESH_HOME=/data/home \
HOME=/data/user \
MESH_PROJECTS_ROOT=/data/projects \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
MESH_PORT=7420 \
MESH_BIND=0.0.0.0 \
MESH_LICENSE_ENFORCEMENT=warn
WORKDIR /app
COPY --from=build /src/package.json ./package.json
COPY --from=build /src/LICENSE ./LICENSE
COPY --from=build /src/THIRD_PARTY_NOTICES.md ./THIRD_PARTY_NOTICES.md
COPY --from=build /src/node_modules ./node_modules
COPY --from=build /src/dist ./dist
COPY --from=build /src/apps/mesh-dashboard/dist ./apps/mesh-dashboard/dist
COPY --from=build /src/tools ./tools
# The crew's eyes: a real Chromium the text-only seats drive through tools/ux-render.mjs. Installed from the
# same pinned playwright-core the driver imports, so the browser and the driver cannot drift, and into /opt —
# never HOME, which is the customer's volume and may be small or read-only.
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright
RUN node "/app/node_modules/playwright-core/cli.js" install --with-deps chromium \
&& ln -s /app/tools/ux-render.mjs /usr/local/bin/ux-render
COPY --from=build /src/apps/mesh-cli/bin ./apps/mesh-cli/bin
COPY --from=build /src/roles ./roles
COPY --from=build /src/schemas ./schemas
COPY --from=build /src/examples ./examples
COPY deploy/docker/entrypoint.sh /usr/local/bin/mesh-entrypoint
# `curule` is the command; `mesh` is the same launcher under the name the product had before, kept so a script or
# a `docker exec ... mesh ...` that predates the rename keeps working.
RUN chmod 0755 /usr/local/bin/mesh-entrypoint /app/apps/mesh-cli/bin/curule.mjs /app/apps/mesh-cli/bin/mesh.mjs \
&& ln -s /app/apps/mesh-cli/bin/curule.mjs /usr/local/bin/curule \
&& ln -s /app/apps/mesh-cli/bin/mesh.mjs /usr/local/bin/mesh
USER mesh
# Render two fixture pages as the mesh uid: the good one must pass, and the bad one must fail with 1. A
# build that cannot render, or that stopped finding a broken page, fails here instead of in a designer's hands.
RUN ux-render --file /app/tools/fixtures/ux-good.html --width 390 --quiet
RUN ux-render --file /app/tools/fixtures/ux-bad.html --width 390 --quiet; test $? -eq 1
VOLUME ["/data"]
EXPOSE 7420
# Liveness only: `/healthz` answers from the event loop and reveals nothing. Readiness is `/readyz`.
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
CMD curl --fail --silent --max-time 4 "http://127.0.0.1:${MESH_PORT}/healthz" >/dev/null || exit 1
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/mesh-entrypoint"]
CMD ["host"]