build(docker): keep keys out of the image and run as non-root by default

The build context excluded only .env, so .env.local and the like or
pinokio/ENVIRONMENT would be baked into image layers; exclude every dotenv
file except .env.example, plus local state and QA output. Pin the base to
CI's Node 24.14.0 by digest, default to the image's node user, and correct
the HOST comment. Log the trusted peers as the operator's when set. README
notes that MCP and ChatGPT sign-in voice are unavailable under Docker, and
how to trust no gateway when widening the publish.
This commit is contained in:
Sameh Khamis committed 2026-10-15 13:07:00 -07:00
1 parent a4e2325ded
commit 3f6fe69c57
5 files changed
+49 -17

No files matched your search

+17 -2
View File
@@ -1,9 +1,24 @@
node_modules
# Keep keys and local state out of the build context: compose bind-mounts the
# checkout at run time, so the image never needs them.
.git
node_modules
dist
.env
.env.*
!.env.example
**/ENVIRONMENT
pinokio/ENVIRONMENT
pinokio/.installed
.gev-cache
.gev-logs
dist
qa-artifacts
qa-shots
screenshots
output
3d-models
.gstack
*.log
.DS_Store
# AppleDouble sidecars from a macOS `tar` copy; `npm test` would otherwise
# collect `._*.test.mjs` as test files.
._*
+4 -3
View File
@@ -235,9 +235,10 @@
[TheSmokeDev](https://github.com/TheSmokeDev) for the credential handling
from #653.
- **Docker install path.** `docker compose up` builds the pinned Node image and
serves the app on `http://localhost:4173` with no local Node install. The repo
is bind-mounted and the container runs as the host user, so `.env`,
- **Docker install path.** `docker compose up` builds on CI's Node 24.14.0 image,
pinned by digest, and serves the app on `http://localhost:4173` with no local
Node install. The repo is bind-mounted and the container runs as the host user
(never root), so `.env`,
`.gev-cache`, and `.gev-logs` persist on the host exactly as they do under
`npm run dev` — Provider Settings included. The launcher
(`scripts/docker-start.mjs`) trusts the container's gateway address for that
+16 -11
View File
@@ -3,11 +3,11 @@
# The dev server is the whole app: Vite serves the client AND runs the API
# proxies (ADS-B, AIS, Overpass, OpenAI Realtime, ...) as middleware, so there
# is nothing else to containerise. See compose.yaml for the usual entry point.
FROM node:24-slim
# Same Node as CI (package.json engines, .github/workflows), pinned by digest.
FROM node:24.14.0-slim@sha256:d8e448a56fc63242f70026718378bd4b00f8c82e78d20eefb199224a4d8e33d8
# Puppeteer is only used by the QA scripts; skip its ~150MB Chromium download.
ENV PUPPETEER_SKIP_DOWNLOAD=1
WORKDIR /app
# `npm test` builds fixtures with `git init`, so the suite cannot run in a
# container without it (71 failures on node:24-slim, all ENOENT on git).
@@ -18,19 +18,24 @@ RUN apt-get update && apt-get install -y --no-install-recommends git && rm -
# git. It is the user's own checkout, so trust that one path.
RUN git config --system --add safe.directory /app
COPY package.json package-lock.json ./
# Never run as root. The image's own user is `node` (1000); compose overrides
# it with the host user so bind-mounted files stay yours.
WORKDIR /app
RUN chown node:node /app
USER node
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci
# compose runs the container as the host user so bind-mounted files stay
# yours, but node_modules is baked in as root. Vite only needs to write its
# dep-optimizer cache, so open up those two directories rather than chowning
# a few hundred MB to a UID we cannot know at build time.
# node_modules belongs to `node`, but compose may run as another UID. Vite only
# needs to write its dep-optimizer cache, so open up those two directories
# rather than chowning a few hundred MB to a UID we cannot know at build time.
RUN mkdir -p node_modules/.vite && chmod 777 node_modules node_modules/.vite
COPY . .
COPY --chown=node:node . .
# Bind to all interfaces, otherwise the published port hits nothing. Vite also
# relaxes allowedHosts when HOST is 0.0.0.0, which is what lets the browser
# reach it as "localhost".
# Bind to all interfaces, otherwise the published port hits nothing. This does
# not widen the Host allowlist: IP addresses and localhost are accepted, other
# names only via GEV_ALLOWED_HOSTS (see build/allowedHosts.js).
ENV HOST=0.0.0.0
ENV PORT=4173
EXPOSE 4173
+4
View File
@@ -171,8 +171,12 @@ Open **`http://localhost:4173`** as above. Everything the app saves stays in the
Back up that folder and you've backed up everything; delete `.env` to forget your keys. Source edits still hot-reload. The port is published to `127.0.0.1` only, so the default matches the native one.
The local MCP server and ChatGPT OAuth voice are not available under Docker; use an OpenAI API key for voice.
Publishing it wider is the [Sharing an instance](#-sharing-an-instance) opt-in, and it hands out **Provider Settings** too: the launcher trusts the container gateway as this machine, and a wider publish delivers everyone else from that same address. For an instance you mean to share, put the keys in `.env` by hand instead. [SECURITY.md](SECURITY.md) has the detail.
When you widen the publish, also set `GEV_KEY_SETUP_TRUSTED_PEERS=none` so the launcher trusts no gateway address.
<details>
<summary>Running on Linux, a NAS, or behind a reverse proxy</summary>
+8 -1
View File
@@ -105,12 +105,19 @@ async function start() {
);
}
const fromOperator = Boolean(
String(process.env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim(),
);
const trustedPeers = resolveTrustedPeers({ routeTable: readRouteTable() });
if (trustedPeers) {
// Set before Vite is imported: the dev server snapshots process.env during
// configuration, and the admission gate reads it per request.
process.env.GEV_KEY_SETUP_TRUSTED_PEERS = trustedPeers;
console.log(`[Docker] Provider Settings trusts the container gateway (${trustedPeers}).`);
console.log(
fromOperator
? `[Docker] Provider Settings trusted peers: ${trustedPeers} (GEV_KEY_SETUP_TRUSTED_PEERS).`
: `[Docker] Provider Settings trusts the container gateway (${trustedPeers}).`,
);
} else {
console.log('[Docker] No default gateway; Provider Settings stays loopback-only.');
}