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.'); }