Repository navigation
Expand file tree
/
Copy pathDockerfile
More file actions
160 lines (132 loc) · 7.34 KB
/
Copy pathDockerfile
File metadata and controls
160 lines (132 loc) · 7.34 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
# =============================================================================
# Cornerstone - Multi-stage Docker build
# =============================================================================
# Stage 1 (app-builder): Runs on the BUILD HOST's native arch to avoid
# QEMU emulation. Installs pure-JS deps and builds everything that produces
# platform-independent output: shared types (tsc), client (webpack), and
# server (tsc). No native addons needed — better-sqlite3 is skipped via
# --ignore-scripts.
# Stage 2 (deps): Runs on the TARGET arch to install production deps with
# native addons. better-sqlite3 v12+ ships prebuilt binaries for
# linuxmusl-arm64, so no compilation tools are needed — prebuild-install
# downloads the correct binary during postinstall.
# Stage 3 (production): Minimal runtime image, no npm/build tools/shell.
# =============================================================================
# Standard build: docker build -t cornerstone .
# =============================================================================
# ---------------------------------------------------------------------------
# Stage 1: App builder (native arch — no QEMU)
# ---------------------------------------------------------------------------
# $BUILDPLATFORM resolves to the Docker host's native architecture (e.g.
# linux/amd64 on GitHub Actions), so webpack and tsc run without QEMU
# emulation. All build output is platform-independent JS/CSS/HTML.
FROM --platform=$BUILDPLATFORM dhi.io/node:24-alpine3.23-dev AS app-builder
WORKDIR /app
# Copy package files for dependency installation
COPY package.json package-lock.json ./
COPY shared/package.json shared/
COPY server/package.json server/
COPY client/package.json client/
# Install all dependencies, skipping postinstall scripts. This avoids
# compiling better-sqlite3 (the only native addon) — it's not needed for
# any build step (tsc/webpack produce platform-independent output).
# --engine-strict enforces the root `engines` Node floor (>=24.11.0, from Babel 8).
RUN --mount=type=cache,target=/root/.npm npm ci --ignore-scripts --engine-strict
# Stamp the release version into package.json (webpack's DefinePlugin reads
# this to embed __APP_VERSION__ in the client bundle).
ARG APP_VERSION=0.0.0-dev
RUN npm pkg set "version=${APP_VERSION}"
# Copy all source (shared, client, server)
COPY tsconfig.base.json ./
COPY shared/ shared/
COPY client/ client/
COPY server/ server/
# Build everything: shared types (tsc) → client (webpack) → server (tsc)
# All output is platform-independent JS — safe to run on build host's arch.
RUN npm run build -w shared && npm run build -w client && npm run build -w server
# ---------------------------------------------------------------------------
# Stage 2: Production dependencies (target arch)
# ---------------------------------------------------------------------------
# Runs on the target platform so better-sqlite3 resolves the correct
# architecture's prebuilt binary. No build tools needed — since v13 the
# prebuilds ship inside the npm tarball itself (prebuilds/linuxmusl-{x64,arm64}.node
# for Alpine), so nothing is downloaded and nothing is compiled.
FROM dhi.io/node:24-alpine3.23-dev AS deps
WORKDIR /app
# Copy package files for dependency installation
COPY package.json package-lock.json ./
COPY shared/package.json shared/
COPY server/package.json server/
COPY client/package.json client/
COPY docs/package.json docs/
# Install production dependencies only.
#
# --ignore-scripts: better-sqlite3 is the ONLY production dependency with an
# install script (every other `hasInstallScript` package in the lockfile is
# dev-only and excluded by --omit=dev). Its package.json declares no install
# script at all — npm *synthesizes* `node-gyp rebuild` because a binding.gyp is
# present. That build is a no-op when a prebuild exists (binding.gyp resolves
# `prebuild_exists` via `node lib/binding.js` and sets target type 'none'), but
# node-gyp still needs python3 just to evaluate gyp. Skipping scripts keeps this
# image toolchain-free; better-sqlite3 resolves prebuilds/<platform>-<arch>.node
# at require() time, so no install-time step is needed.
# --engine-strict enforces the root `engines` Node floor (>=24.11.0, from Babel 8).
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev --ignore-scripts --engine-strict
# Fail the build loudly if better-sqlite3 has no prebuilt binary for this
# target. Without this, a missing prebuild would surface as a runtime crash in
# the production image instead of a build failure here.
RUN node -e "const p=require('/app/node_modules/better-sqlite3/lib/binding.js').getPrebuildPath(); if(!p){console.error('FATAL: no better-sqlite3 prebuild for '+process.platform+'-'+process.arch);process.exit(1)} console.log('better-sqlite3 prebuild: '+p)"
# Ensure workspace node_modules directories exist even when npm hoists all
# production dependencies to the root node_modules/. Without this, the
# production stage's COPY --from=deps /app/server/node_modules/ fails with
# "not found" whenever no packages remain workspace-local.
RUN mkdir -p /app/server/node_modules /app/client/node_modules /app/shared/node_modules
# Create backups directory (for copying into production stage)
RUN mkdir -p /backups
# ---------------------------------------------------------------------------
# Stage 3: Production (no shell — exec form only)
# ---------------------------------------------------------------------------
FROM dhi.io/node:24-alpine3.23 AS production
# Create data directory (WORKDIR creates intermediate dirs without needing shell)
WORKDIR /app/data
WORKDIR /app
# Copy runtime libraries needed by native addons (better-sqlite3 requires libgcc/libstdc++)
COPY --from=deps /usr/lib/libgcc_s.so.1 /usr/lib/
COPY --from=deps /usr/lib/libstdc++.so.6* /usr/lib/
# Copy package files (needed for workspace resolution)
COPY package.json ./
COPY shared/package.json shared/
COPY server/package.json server/
COPY client/package.json client/
COPY docs/package.json docs/
# Copy production node_modules from deps (npm hoists most deps to root,
# but some may remain in workspace-specific node_modules due to version constraints)
COPY --from=deps /app/node_modules/ node_modules/
COPY --from=deps /app/server/node_modules/ server/node_modules/
# Copy built artifacts from app-builder
COPY --from=app-builder /app/shared/dist/ shared/dist/
COPY --from=app-builder /app/server/dist/ server/dist/
COPY --from=app-builder /app/client/dist/ client/dist/
# Copy SQL migration files (tsc does not copy non-TS assets)
COPY --from=app-builder /app/server/src/db/migrations/ server/dist/db/migrations/
# Create backups directory with correct ownership
COPY --from=deps --chown=node:node /backups /backups
# Expose server port
EXPOSE 3000
# SQLite data volume
VOLUME ["/app/data"]
# Backups volume
VOLUME ["/backups"]
# Environment defaults
ENV NODE_ENV=production
ENV PORT=3000
ENV HOST=0.0.0.0
ENV DATABASE_URL=/app/data/cornerstone.db
ENV LOG_LEVEL=info
ENV CURRENCY=EUR
# Health check — exec form required (DHI production image has no /bin/sh)
# Uses /api/health/ready which verifies DB access and password hashing round-trip
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD ["node", "-e", "fetch('http://localhost:3000/api/health/ready').then(r=>{if(!r.ok)throw r.status}).catch(()=>process.exit(1))"]
# Start the server
CMD ["node", "server/dist/server.js"]