-
-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathDockerfile
More file actions
315 lines (290 loc) · 15.9 KB
/
Copy pathDockerfile
File metadata and controls
315 lines (290 loc) · 15.9 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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# ============================================================
# Stage 1: Build React frontend + Node.js backend
# ============================================================
FROM node:26.7.0-slim@sha256:5758d367d7b4f48b73a9bb3530e687e47efb289f3b43f9c0450a25225ae0db5d AS acarshub-react-builder
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
WORKDIR /workspace
# Build number comes from CI (GitHub Actions run number).
# Version strings are read directly from the workspace package.json files by
# vite.config.ts (frontend) and config.ts (backend) — no ARG injection needed.
ARG BUILD_NUMBER=0
# ── Dependency layer ─────────────────────────────────────────
# Copy manifests and lockfile before source so Docker can cache
# the npm ci layer independently of source changes.
COPY package.json package-lock.json tsconfig.json tsconfig.base.json ./
COPY acarshub-react/package.json ./acarshub-react/package.json
COPY acarshub-backend/package.json ./acarshub-backend/package.json
COPY acarshub-types/package.json ./acarshub-types/package.json
# Install all workspace dependencies (devDeps required for tsc/vite build tools)
# --loglevel=error suppresses deprecation warnings from transitive deps in @lhci/cli
# and drizzle-kit (both at latest versions; upstream fixes required to remove them)
#
# Root package.json's "allowScripts" denies better-sqlite3's install
# script here. That script ("node-gyp rebuild") is a phantom entry from
# an upstream npm registry-normalization bug (npm/cli#8714): the package
# ships working prebuilt N-API binaries under prebuilds/ and explicitly
# opts out via "gypfile": false, but npm's publish-time manifest
# normalization re-adds the script to the registry packument anyway, so
# npm still tries to run it — and it would fail here since this slim
# image has no Python/build toolchain. Denying it is correct regardless
# of image/npm version: the bundled prebuild is used either way. (This
# requires an npm new enough to actually enforce allowScripts — the one
# bundled with node:26.5.1-slim does; older npm versions treat it as
# advisory-only and would silently still run the phantom script.)
RUN set -xe && \
npm ci --include=dev --loglevel=error
# ── Source layer ─────────────────────────────────────────────
# Copied after npm ci so the dependency layer survives source edits.
COPY acarshub-react/ ./acarshub-react/
COPY acarshub-backend/ ./acarshub-backend/
COPY acarshub-types/ ./acarshub-types/
# Re-declare ARG after FROM so it is in scope for the RUN
ARG BUILD_NUMBER=0
# VITE_BUILD_NUMBER is a compile-time Vite variable, inlined into the frontend
# bundle by `vite build` (see acarshub-react/src/utils/version.ts). It is
# declared as ENV rather than exported inside a single RUN because the build is
# split across several RUN layers below (WORKDIR instead of `cd`, per DL3003),
# and a shell `export` would not survive across them. This is a builder stage,
# so the variable never reaches the runtime image.
ENV VITE_BUILD_NUMBER="${BUILD_NUMBER}"
RUN set -xe && \
# Build each workspace individually so we can skip generate-sprites for the
# React app. The sprite sheets (PNG + WebP) are pre-generated static assets
# committed to the repository (acarshub-react/src/assets/sprites/) and are
# already present in the build context via the COPY above — regenerating them
# here is wasted work.
#
# History: arm64 builds were broken because package-lock.json had been
# regenerated while node_modules was present with only x64 optional packages
# installed (npm bug #4828). npm ci faithfully reproduces the lockfile, so
# the arm64 sharp/rollup prebuilts were never installed, causing:
# "Could not load the 'sharp' module using the linux-arm64 runtime"
# "Cannot find module @rollup/rollup-linux-arm64-gnu"
# The root fix was regenerating the lockfile from scratch (no node_modules),
# which causes npm to record ALL platform variants of optional deps.
# Skipping generate-sprites is a permanent defensive layer: it removes any
# dependency on sharp's native binary running correctly under QEMU emulation,
# and avoids re-triggering the issue if the lockfile is ever corrupted again.
# `vite build` produces an identical dist/ output without touching sharp.
npm run build --workspace=@acarshub/types
# `vite build` is invoked directly (not via `npm run build --workspace`) to skip
# generate-sprites, per the rationale above. WORKDIR rather than `cd` satisfies
# DL3003; the following WORKDIR returns to /workspace because the remaining
# staging steps use paths relative to the workspace root.
WORKDIR /workspace/acarshub-react
RUN set -xe && \
npx vite build
WORKDIR /workspace
RUN set -xe && \
npm run build --workspace=@acarshub/backend && \
# Bundle the backend into a single ESM file with esbuild.
# better-sqlite3 and zeromq are marked external because they contain native
# .node addons that cannot be inlined into a JS bundle — they must be loaded
# from disk by Node.js at runtime. All other production dependencies
# (fastify, socket.io, drizzle-orm, pino, pino-pretty, zod, @airframes, …)
# are inlined, so they do not need to be present in the runtime node_modules.
# node:* built-ins are always external.
npx esbuild acarshub-backend/src/server.ts \
--bundle \
--platform=node \
--format=esm \
--target=node22 \
--external:better-sqlite3 \
--external:zeromq \
--external:'node:*' \
--banner:js="import { createRequire } from 'module'; const require = createRequire(import.meta.url);" \
--outfile=/backend/server.bundle.mjs && \
# Bundle the migration worker as a separate ESM file.
# This file is spawned as a child process by runMigrationsInWorker() so that
# SQLite VACUUM runs off the main event loop (keeping Socket.IO responsive
# during long migrations). It must be a standalone file — it cannot be
# inlined into server.bundle.mjs because spawn() needs a real path on disk.
# zeromq is not needed by the migrator so it is not marked external here.
npx esbuild acarshub-backend/src/db/migrate-worker.ts \
--bundle \
--platform=node \
--format=esm \
--target=node22 \
--external:better-sqlite3 \
--external:'node:*' \
--banner:js="import { createRequire } from 'module'; const require = createRequire(import.meta.url);" \
--outfile=/backend/migrate-worker.mjs && \
# Stage React SPA output
mkdir -p /webapp/dist && \
cp -r ./acarshub-react/dist/* /webapp/dist/ && \
# Stage Drizzle SQL migration files (needed by the migrator at runtime)
cp -r ./acarshub-backend/drizzle/ /backend/drizzle/ && \
# Stage native addon runtime files.
# Only better-sqlite3 and zeromq (plus the cmake-ts prebuilt-loader that
# zeromq depends on) are needed at runtime. All other production deps
# are already inlined in server.bundle.mjs.
#
# better-sqlite3 runtime deps (N-API since v13 — see the allowScripts
# comment near npm ci above for why this layout changed from v12's
# build/Release/ + the generic 'bindings' package loader):
# prebuilds/ — one prebuilt .node addon per platform/arch (pruned below)
# lib/ — JS wrapper; lib/binding.js resolves prebuilds/<platform>.node
# directly, no longer via the 'bindings' package, which is
# why that package (and its file-uri-to-path transitive dep)
# is no longer copied here — npm itself no longer installs
# either now that nothing depends on them.
#
# zeromq runtime deps:
# build/ — prebuilt .node addons (all platforms; pruned below)
# lib/ — JS wrapper
# cmake-ts/ — prebuilt-addon loader (reads build/manifest.json)
mkdir -p \
/addon-deps/better-sqlite3 \
/addon-deps/zeromq \
/addon-deps/cmake-ts && \
cp -r node_modules/better-sqlite3/prebuilds /addon-deps/better-sqlite3/prebuilds && \
cp -r node_modules/better-sqlite3/lib /addon-deps/better-sqlite3/lib && \
cp node_modules/better-sqlite3/package.json /addon-deps/better-sqlite3/ && \
cp -r node_modules/zeromq/build /addon-deps/zeromq/build && \
cp -r node_modules/zeromq/lib /addon-deps/zeromq/lib && \
cp node_modules/zeromq/package.json /addon-deps/zeromq/ && \
cp -r node_modules/cmake-ts/. /addon-deps/cmake-ts/ && \
# Prune zeromq prebuilts that will never be used in this Linux container:
# win32 and darwin — wrong OS entirely
# musl variants — docker-baseimage:base is Debian/glibc, not Alpine
# other CPU arch — the image is built natively; the wrong arch is dead weight
# src/ — C++ source, not needed post-compilation
rm -rf \
/addon-deps/zeromq/build/win32 \
/addon-deps/zeromq/build/darwin \
/addon-deps/zeromq/src && \
{ find /addon-deps/zeromq/build/linux -type d -name "musl-*" -exec rm -rf {} + 2>/dev/null || true; } && \
if [ "$(uname -m)" = "x86_64" ]; then \
rm -rf /addon-deps/zeromq/build/linux/arm64; \
else \
rm -rf /addon-deps/zeromq/build/linux/x64; \
fi && \
# Prune better-sqlite3 prebuilds down to the one matching this
# container's platform (Debian/glibc, so no musl variant either),
# same rationale as the zeromq pruning above.
if [ "$(uname -m)" = "x86_64" ]; then \
find /addon-deps/better-sqlite3/prebuilds -type f ! -name "linux-x64.node" -delete; \
else \
find /addon-deps/better-sqlite3/prebuilds -type f ! -name "linux-arm64.node" -delete; \
fi && \
# cleanup js map files, those are just for viewing the code
find /addon-deps -name "*.map" -delete
# ============================================================
# Stage 2: Runtime image
# ============================================================
FROM ghcr.io/sdr-enthusiasts/docker-baseimage:base
ARG BUILD_NUMBER=0
# SHELL must come *after* any ARG/ENV in the stage. hadolint 2.15.x resets its
# shell-dialect tracking to POSIX sh when an ARG or ENV follows SHELL, which
# makes every later RUN be linted as sh and spuriously reports SC3054 for the
# bash arrays below.
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Copy the Node.js runtime from the builder.
# node:slim is Debian-based (same ABI family as docker-baseimage:base) so the
# binary and its pre-compiled native add-ons are directly compatible.
#
# We copy only the node binary and the npm module directory - NOT /usr/local/bin/npm.
# In node:slim, /usr/local/bin/npm is a symlink whose target (npm-cli.js) uses
# __dirname to locate ../lib/cli.js. COPY --from dereferences symlinks, so the
# file lands at /usr/local/bin/npm with __dirname=/usr/local/bin and the relative
# require('../lib/cli.js') resolves to /usr/local/lib/cli.js (wrong).
# A shell wrapper that invokes npm-cli.js by absolute path keeps __dirname correct.
# Copy only the node binary — npm is not needed in the runtime image.
COPY --from=acarshub-react-builder /usr/local/bin/node /usr/local/bin/node
# hadolint ignore=DL3008,SC2086
RUN set -x && \
KEPT_PACKAGES=() && \
KEPT_PACKAGES+=(nginx-light) && \
KEPT_PACKAGES+=(libnginx-mod-http-brotli-filter) && \
KEPT_PACKAGES+=(rrdtool) && \
apt-get update && \
apt-get install -y --no-install-recommends \
"${KEPT_PACKAGES[@]}" \
&& \
apt-get clean -q -y && \
rm -rf /tmp/* /var/lib/apt/lists/* /var/cache/* && \
# Runtime directories expected by s6 services and the Node backend
mkdir -p /run/acars /webapp/data/ /backend
# Package manifests are needed at runtime for version reporting (config.ts reads
# package.json files via process.cwd() to determine the container/backend/frontend
# version strings). The lockfile and workspace source files are not needed.
WORKDIR /backend
COPY package.json ./
COPY acarshub-backend/package.json ./acarshub-backend/
COPY acarshub-types/package.json ./acarshub-types/
COPY acarshub-react/package.json ./acarshub-react/
# Copy the esbuild bundle and its native addon runtime dependencies.
#
# The bundle (server.bundle.mjs) contains all pure-JS production dependencies
# inlined — fastify, socket.io, drizzle-orm, pino, pino-pretty, zod,
# @airframes/acars-decoder, etc. Only the two native addons (better-sqlite3
# and zeromq) remain as external packages that Node.js must resolve from disk.
#
# The native addon runtime files were staged and pruned in the builder:
# - cross-platform zeromq prebuilts removed (win32, darwin, musl, other arch)
# - better-sqlite3 prebuilds pruned to just this platform/arch (N-API since
# v13 — see the allowScripts comment near npm ci above for the full story)
#
# This replaces the entire "apt-get install compilers → npm ci → apt-get purge"
# block from the old approach — no compilers are needed in the runtime stage
# because the native addons were already compiled in the builder stage, which
# already has all build tools present for the tsc/vite/esbuild steps.
COPY --from=acarshub-react-builder /backend/server.bundle.mjs ./server.bundle.mjs
COPY --from=acarshub-react-builder /backend/migrate-worker.mjs ./migrate-worker.mjs
COPY --from=acarshub-react-builder /addon-deps/better-sqlite3 ./node_modules/better-sqlite3
COPY --from=acarshub-react-builder /addon-deps/zeromq ./node_modules/zeromq
COPY --from=acarshub-react-builder /addon-deps/cmake-ts ./node_modules/cmake-ts
# React SPA served by nginx
COPY --from=acarshub-react-builder /webapp/dist/ /webapp/dist/
# Pre-compress text assets so nginx can serve them via gzip_static without
# runtime compression cost. The originals are kept alongside the .gz files
# so nginx can still serve uncompressed versions to clients that don't send
# Accept-Encoding: gzip. Targets JS, CSS, and GeoJSON (the two large overlay
# files – TRACONBoundaries and FIRBoundaries – compress ~80% as JSON text).
RUN find /webapp/dist/assets \
\( -name "*.js" -o -name "*.css" -o -name "*.geojson" \) \
-exec gzip -9 --keep {} \;
# Drizzle SQL migration files — read from disk by the migrator at startup
COPY --from=acarshub-react-builder /backend/drizzle/ /backend/drizzle/
COPY rootfs/ /
RUN set -x && \
# Read the container version directly from the workspace root package.json.
# This is the single source of truth — no ARG VERSION injection from CI.
ACARS_VERSION=$(node -p "JSON.parse(require('fs').readFileSync('/backend/package.json','utf8')).version") && \
ACARS_BUILD="${BUILD_NUMBER}" && \
echo "ACARS Hub (Node.js): v${ACARS_VERSION} Build ${ACARS_BUILD}" && \
# Standard version files used by base-image infrastructure
printf "v%sBuild%s" "$ACARS_VERSION" "$ACARS_BUILD" > /acarshub_version && \
printf "v%s Build %s\nv%sBuild%s" \
"$ACARS_VERSION" "$ACARS_BUILD" \
"$ACARS_VERSION" "$ACARS_BUILD" > /version && \
# Ensure all s6 scripts and the healthcheck are executable
find /etc/s6-overlay/scripts -name "*.sh" -exec chmod +x {} \; && \
chmod +x /scripts/healthcheck.sh
EXPOSE 80
# Default UDP listen ports for each decoder type (informational; Docker does
# not require EXPOSE for UDP to work, but this documents the defaults).
EXPOSE 5550/udp
EXPOSE 5555/udp
EXPOSE 5556/udp
EXPOSE 5557/udp
EXPOSE 5558/udp
ENV ENABLE_ACARS="false" \
ENABLE_VDLM="false" \
ENABLE_ADSB="false" \
MIN_LOG_LEVEL=4 \
DB_SAVEALL="true" \
ENABLE_RANGE_RINGS="true" \
ADSB_URL="http://tar1090/data/aircraft.json" \
PORT=8888 \
ACARSHUB_NGINX_PORT=80 \
ACARSHUB_DB="/run/acars/messages.db" \
GROUND_STATION_PATH="/webapp/data/ground-stations.json" \
MESSAGE_LABELS_PATH="/webapp/data/metadata.json" \
AIRLINES_PATH="/webapp/data/airlines.json" \
ACARS_CONNECTIONS="udp" \
VDLM_CONNECTIONS="udp" \
HFDL_CONNECTIONS="udp" \
IMSL_CONNECTIONS="udp" \
IRDM_CONNECTIONS="udp"
HEALTHCHECK --start-period=3600s --interval=600s CMD ["/scripts/healthcheck.sh"]