Repository navigation
Expand file tree
/
Copy pathDockerfile
More file actions
181 lines (151 loc) · 7.84 KB
/
Copy pathDockerfile
File metadata and controls
181 lines (151 loc) · 7.84 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
# Build stage
FROM node:24.22.1-alpine3.24 AS builder
WORKDIR /app
# Native-module build toolchain (builder stage only — never in the runtime image).
# Most native deps (better-sqlite3, bcrypt) download prebuilt binaries, but re2
# has no prebuilt for every target (notably Alpine/musl on arm), so node-gyp must
# be able to compile it from source. `build-base` (C/C++ compiler) is what's new
# here; `python3` is node-gyp's other requirement (also installed in the runtime
# stage, but the stages are independent). Also a fallback for any other native
# dep missing a prebuilt for the target arch.
RUN apk add --no-cache build-base python3
# Copy package files
COPY package*.json ./
# Install dependencies
# Use npm install instead of npm ci to avoid optional dependency bug
# better-sqlite3 will download pre-built binaries for the target platform
# Use cache mount to speed up repeated builds
# --legacy-peer-deps needed for vitest peer dependency conflicts
#
# PUPPETEER_SKIP_DOWNLOAD=true: puppeteer's npm postinstall downloads a Chrome
# binary. This image is built for linux/arm64 under QEMU emulation, where that
# postinstall crashes with SIGILL ("qemu: uncaught target signal 4") and takes
# the whole build with it. Puppeteer is a devDependency used only by two ad-hoc
# screenshot helpers (scripts/capture-*-screenshots.js) — neither the runtime
# image nor the build needs the browser binary.
#
# .npmrc already sets `puppeteer_skip_download=true`, but that is no longer
# sufficient: npm now reports it as `Unknown project config` and stops
# forwarding it as `npm_config_puppeteer_skip_download`, so puppeteer never
# sees it and runs the download anyway. The environment variable is read
# directly by puppeteer and does not depend on npm's config forwarding.
# Dockerfile.armv7 has always set it this way — which is why the armv7 leg
# kept building while this one broke (v4.14.2-rc1).
RUN --mount=type=cache,target=/root/.npm \
PUPPETEER_SKIP_DOWNLOAD=true \
npm install --legacy-peer-deps
# Verify protobufs are present (fail fast if git submodule wasn't initialized)
# Copy protobufs first as they rarely change
COPY protobufs ./protobufs
RUN if [ ! -f "protobufs/meshtastic/mesh.proto" ]; then \
echo "ERROR: Protobuf files not found! Git submodule may not be initialized."; \
echo "Run: git submodule update --init --recursive"; \
exit 1; \
fi
# ATAK V2 zstd dictionaries (#4317) — runtime data files from the
# takpacket-sdk git submodule; only the dictionaries directory is needed
COPY takpacket-sdk/dictionaries ./takpacket-sdk/dictionaries
RUN if [ ! -f "takpacket-sdk/dictionaries/dict_non_aircraft.zstd" ]; then \
echo "ERROR: TAKPacket-SDK dictionaries not found! Git submodule may not be initialized."; \
echo "Run: git submodule update --init --recursive"; \
exit 1; \
fi
# Copy config files and source needed for builds
COPY tsconfig.json tsconfig.server.json tsconfig.node.json vite.config.ts index.html embed.html ./
COPY src ./src
COPY public ./public
# Build the React application first (always for root, will be rewritten at runtime)
# Vite clears dist directory, so this must come before server build
RUN --mount=type=cache,target=/app/node_modules/.vite \
npm run build
# Build the server last so it doesn't get overwritten by Vite
# TypeScript server build will add to dist directory without clearing it
RUN npm run build:server
# Drop devDependencies before node_modules is copied into the runtime image
# (#5692). The build above needs them (tsc, vite, ...); the server never loads
# them, and shipping them put ~hundreds of MB of unreachable code — and its scan
# findings — into every image. Pruning here, in the builder, rather than after
# the COPY in the runtime stage, is what actually shrinks the image: a prune in
# a later layer would leave the full tree in the layer underneath. Native
# modules (better-sqlite3, bcrypt, re2, zstd-napi, serialport bindings) are
# dependencies/optionalDependencies, so their compiled binaries are kept as built.
# `npm run check:imports` in CI fails if a module the server loads is not listed
# in dependencies/optionalDependencies.
RUN npm prune --omit=dev --legacy-peer-deps
# Production stage
FROM node:24.22.1-alpine3.24
WORKDIR /app
# Install curl (for healthchecks), Python and dependencies for Apprise
# Create python symlink for user scripts that use #!/usr/bin/env python
RUN apk add --no-cache \
curl \
tzdata \
unzip \
python3 \
py3-pip \
py3-requests \
supervisor \
su-exec \
&& ln -sf /usr/bin/python3 /usr/bin/python \
&& python3 -m venv /opt/apprise-venv \
&& /opt/apprise-venv/bin/pip install --no-cache-dir apprise "paho-mqtt<2.0" meshtastic meshcore-cli \
&& ln -sf /opt/apprise-venv/bin/meshtastic /usr/local/bin/meshtastic \
&& ln -sf /opt/apprise-venv/bin/meshcore-cli /usr/local/bin/meshcore-cli \
&& ln -sf /opt/apprise-venv/bin/meshcli /usr/local/bin/meshcli \
&& ln -sf /usr/bin/python3 /usr/local/bin/python3
# npm stays in the runtime image: `npm start` launches the server, and the
# user-script dependency installer (scriptDependencyService) runs `npm install`.
# The npm bundled with the base image carries its own copy of `tar`; 11.19.0
# (node 24.21.0) bundles tar 7.5.19, which GHSA-r292-9mhp-454m covers. Pin a
# release in the same major that bundles a fixed tar (11.21.0 -> tar 7.5.22).
ARG NPM_VERSION=11.21.0
RUN npm install -g "npm@${NPM_VERSION}" \
&& npm cache clean --force \
&& rm -rf /root/.npm
# Copy package files
COPY package*.json ./
# Copy node_modules from builder (production dependencies only, with their
# compiled native modules — see the prune at the end of the builder stage)
COPY --from=builder /app/node_modules ./node_modules
# Copy built assets from builder stage
COPY --chown=node:node --from=builder /app/dist ./dist
# Copy protobuf definitions needed by the server
COPY --from=builder /app/protobufs ./protobufs
# Copy ATAK V2 zstd dictionaries needed by the server (#4317) — dictionaries
# only; the rest of the submodule (testdata, docs, language bindings) is not
# needed at runtime
COPY --from=builder /app/takpacket-sdk/dictionaries ./takpacket-sdk/dictionaries
# Copy admin password reset script
COPY reset-admin.mjs /app/reset-admin.mjs
# Create data directory for SQLite database and Apprise configs
RUN mkdir -p /data/apprise-config /data/scripts && chown -R node:node /data
# Create supervisor configuration to run both Node.js and Apprise
RUN mkdir -p /etc/supervisor/conf.d
COPY docker/supervisord.conf /etc/supervisord.conf
# Create Apprise API wrapper script
COPY docker/apprise-api.py /app/apprise-api.py
RUN chmod +x /app/apprise-api.py
# Copy and set up entrypoint script
COPY docker/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Expose ports
# 3001: MeshMonitor Express server
# 8000: Internal Apprise API (not exposed to host by default)
# 8088: ATAK/CoT feed (settings-gated, default off — see docs/features/atak.md)
EXPOSE 3001 8000 8088
# Set environment variables
ENV NODE_ENV=production
ENV PORT=3001
ENV APPRISE_CONFIG_DIR=/data/apprise-config
ENV APPRISE_STATEFUL_MODE=simple
# No `USER` directive on purpose (#5692): the entrypoint starts as root only to
# renumber node for PUID/PGID, grant node the mapped serial devices' groups and
# fix /data ownership on volumes written by older root-run images, then drops
# to `node` with su-exec before it starts supervisord. supervisord, the server
# and Apprise all run as node. To start fully unprivileged instead, run the
# container with `user: "1000:1000"` (plus `group_add` for serial devices);
# the entrypoint detects that and skips the root-only steps.
# Use entrypoint to deploy scripts before starting supervisor
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
# Run supervisor to manage both processes
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]