From 3f6fe69c57d34e89378e8fbca2b44ff78f52b93e Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 15 Oct 2026 13:07:00 -0700 Subject: [PATCH] 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. --- .dockerignore | 19 +++++++++++++++++-- CHANGELOG.md | 7 ++++--- Dockerfile | 27 ++++++++++++++++----------- README.md | 4 ++++ scripts/docker-start.mjs | 9 ++++++++- 5 files changed, 49 insertions(+), 17 deletions(-) diff --git a/.dockerignore b/.dockerignore index 3f5bc8a9d..174f7990a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -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. ._* diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e2cea294..2a7fc91d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Dockerfile b/Dockerfile index b1b0876d1..d34f4887a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index b90b37ab9..46007e4c2 100644 --- a/README.md +++ b/README.md @@ -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. +
Running on Linux, a NAS, or behind a reverse proxy diff --git a/scripts/docker-start.mjs b/scripts/docker-start.mjs index 47519af97..3935f2b3b 100644 --- a/scripts/docker-start.mjs +++ b/scripts/docker-start.mjs @@ -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.'); }