Repository navigation
Expand file tree
/
Copy pathDockerfile
More file actions
234 lines (181 loc) · 9.58 KB
/
Copy pathDockerfile
File metadata and controls
234 lines (181 loc) · 9.58 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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# check=skip=InvalidDefaultArgInFrom
# =============================================================================
# Production Dockerfile (multi-target: backend | cdc | yjs)
# =============================================================================
# One file builds all three Node.js service images; CI selects a service with
# `--target <name>` (see `target` in infra/config/services.config.ts, threaded
# through print-deploy-env into .github/workflows/deploy.yml). Shared stages:
#
# base : node:<NODE_VERSION>-alpine + pnpm from npm, at the version the
# root package.json `packageManager` field pins
# runtime : node:<NODE_VERSION>-alpine + non-root `app` user +
# RELEASE_SHA/NODE_ENV, the parent of every production target
#
# manifests: all workspace manifests + lockfile (shared by every service;
# `--filter` scopes each install, and lockfile changes already
# invalidate all services)
#
# Per service: `<svc>-deps` (prod install) / `<svc>-builder` (full install +
# rolldown build) → `<svc>` (production target). Targets: `backend` (also reused by
# the `mcp`, `oauth` and `jobs` services through `reusesImageOf`, each with its
# own MODE and port), `cdc`, `yjs`. Not used in local dev.
#
# NODE_VERSION is the root package.json `engines.node`, so the images run the
# Node CI tested. It has no default (the `check=skip` line on top keeps Docker
# from warning about that); a local build passes it:
# docker build --build-arg NODE_VERSION="$(jq -r .engines.node package.json)" --target backend .
# =============================================================================
ARG NODE_VERSION
# -----------------------------------------------------------------------------
# Shared: base image with pnpm
# -----------------------------------------------------------------------------
FROM node:${NODE_VERSION}-alpine AS base
WORKDIR /app
# pnpm comes from npm at the version pinned by the root package.json
# `packageManager` field (the pin pnpm/action-setup reads in CI), minus any
# `+sha` suffix. Node 25+ images no longer ship corepack.
COPY package.json ./
RUN npm install -g --no-fund --no-audit "$(node -p "require('./package.json').packageManager.split('+')[0]")"
# -----------------------------------------------------------------------------
# Shared: production runtime base (non-root user, release metadata)
# -----------------------------------------------------------------------------
FROM node:${NODE_VERSION}-alpine AS runtime
RUN addgroup --system --gid 1001 app && \
adduser --system --uid 1001 app
WORKDIR /app
# Build-time release identifier (Git SHA from CI). Surfaced at runtime via
# `/health` so the deploy verifier can confirm the new code is actually live.
ARG RELEASE_SHA=unknown
ENV RELEASE_SHA=${RELEASE_SHA}
ENV NODE_ENV=production
USER app
# -----------------------------------------------------------------------------
# Shared: workspace manifests for dependency installs
# -----------------------------------------------------------------------------
# One stage for all services: each install below is scoped by `--filter`, and
# pnpm-lock.yaml pins resolution, so unused manifests can't change what gets
# installed: they only ride along for the workspace layout.
FROM base AS manifests
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY backend/package.json ./backend/
COPY shared/package.json ./shared/
COPY sdk/package.json ./sdk/
COPY cdc/package.json ./cdc/
COPY yjs/package.json ./yjs/
# =============================================================================
# backend: Hono API server
# =============================================================================
# Install production dependencies only (--ignore-scripts avoids native build issues). The filter has no
# `...`: that would follow workspace links, devDependencies included, and install the `dependencies` of
# shared, sdk, cdc and yjs too, which the bundle inlines.
FROM manifests AS backend-deps
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --prod --ignore-scripts --filter backend
# Install all dependencies (including dev for build) and build with rolldown
FROM manifests AS backend-builder
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --ignore-scripts --filter backend...
# Every workspace tsconfig extends the root one, and the build stops without it.
COPY tsconfig.json ./
COPY backend/ ./backend/
COPY shared/ ./shared/
COPY sdk/ ./sdk/
COPY locales/ ./locales/
COPY cdc/ ./cdc/
COPY yjs/ ./yjs/
COPY json/ ./json/
WORKDIR /app/backend
RUN pnpm build
# Production image
FROM runtime AS backend
# Built application
COPY --from=backend-builder --chown=app:app /app/backend/dist ./backend/dist
COPY --from=backend-builder --chown=app:app /app/backend/package.json ./backend/
# Production dependencies
COPY --from=backend-deps --chown=app:app /app/node_modules ./node_modules
COPY --from=backend-deps --chown=app:app /app/backend/node_modules ./backend/node_modules
# Runtime data. The bundle inlines the shared workspace into backend/dist, and the deps stage installs
# the backend's own `dependencies` only, so neither shared's source nor its node_modules is here.
COPY --from=backend-builder --chown=app:app /app/locales ./locales
COPY --from=backend-builder --chown=app:app /app/json ./json
# Drizzle migrations
COPY --chown=app:app backend/drizzle ./backend/drizzle
# GeoIP databases download here at runtime (backend/src/lib/geoip.ts); the image ships none.
USER root
RUN mkdir -p backend/geoip && chown app:app backend/geoip
USER app
ENV PORT=4000
EXPOSE 4000
# Compose injects its own healthcheck on deploy (infra/compose/infrastructure.ts);
# this baked one covers standalone runs. ${PORT} keeps it honest when the image
# is reused on another port (mcp runs it with PORT=4003, jobs with PORT=4006).
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:${PORT:-4000}/health || exit 1
WORKDIR /app/backend
# The passkey library asks Web Crypto which algorithms it supports at load, which Node 26 answers with experimental warnings.
CMD ["node", "--disable-warning=ExperimentalWarning", "dist/main.js"]
# =============================================================================
# cdc: Change Data Capture worker
# =============================================================================
# Connects to PostgreSQL logical replication and forwards to backend WebSocket.
# Install production dependencies only (--ignore-scripts avoids native build issues)
FROM manifests AS cdc-deps
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --prod --ignore-scripts --filter cdc-worker
# Install all dependencies (including dev for build) and build with rolldown
# Install cdc and shared deps; backend deps needed for bundling backend/src imports
FROM manifests AS cdc-builder
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --ignore-scripts --filter cdc-worker... --filter backend...
COPY tsconfig.json ./
COPY cdc/ ./cdc/
COPY backend/src ./backend/src
COPY shared/ ./shared/
COPY sdk/ ./sdk/
COPY locales/ ./locales/
WORKDIR /app/cdc
RUN pnpm build
# Production image (internal-only: no EXPOSE, no baked healthcheck)
FROM runtime AS cdc
COPY --from=cdc-builder --chown=app:app /app/cdc/dist ./cdc/dist
COPY --from=cdc-builder --chown=app:app /app/cdc/package.json ./cdc/
COPY --from=cdc-deps --chown=app:app /app/node_modules ./node_modules
COPY --from=cdc-deps --chown=app:app /app/cdc/node_modules ./cdc/node_modules
WORKDIR /app/cdc
CMD ["node", "dist/cdc-worker.js"]
# =============================================================================
# yjs: collaborative editing relay worker
# =============================================================================
# Pure binary passthrough: no Y.Doc instantiation on server.
# Install production dependencies only (--ignore-scripts avoids native build issues)
FROM manifests AS yjs-deps
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --prod --ignore-scripts --filter yjs-worker
# Install all dependencies (including dev for build) and build with rolldown
# Install yjs and shared deps; backend deps needed for bundling backend/src imports
FROM manifests AS yjs-builder
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --ignore-scripts --filter yjs-worker... --filter backend...
COPY tsconfig.json ./
COPY yjs/ ./yjs/
COPY backend/src ./backend/src
COPY shared/ ./shared/
# AppError's messages bundle the backend translations
COPY locales/ ./locales/
WORKDIR /app/yjs
RUN pnpm build
# Production image
FROM runtime AS yjs
COPY --from=yjs-builder --chown=app:app /app/yjs/dist ./yjs/dist
COPY --from=yjs-builder --chown=app:app /app/yjs/package.json ./yjs/
COPY --from=yjs-deps --chown=app:app /app/node_modules ./node_modules
COPY --from=yjs-deps --chown=app:app /app/yjs/node_modules ./yjs/node_modules
# WebSocket port
EXPOSE 4002
# Compose injects its own healthcheck on deploy (infra/compose/infrastructure.ts);
# this baked one covers standalone runs.
HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:4002/health || exit 1
WORKDIR /app/yjs
# lib0 probes localStorage at load; without Web Storage it uses its in-memory store and Node 26 prints no warning.
CMD ["node", "--no-experimental-webstorage", "dist/yjs-worker.js"]