diff --git a/.dockerignore b/.dockerignore index 8d9dac9..15f04ee 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,6 +11,9 @@ tmp .claude docs landing-e2e + +# tools/ IS needed: the build stage copies docker-install-deps.sh from it. +# The mock API servers in there are dev-only but too small to be worth excluding. **/*.spec.ts test-output .env diff --git a/.env.example b/.env.example index 868e3b1..28eed70 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,19 @@ -# Base URL of the Pireon API, without a trailing slash. Read on the server -# only: the browser never talks to the API directly. +# Copy to `.env`. Compose reads it on its own — for the runtime settings below +# and for the build-time token. `.env` is gitignored; this file is not. + +# Base URL of the Pireon API, without a trailing slash. Read on the server only: +# the browser never talks to the API directly. PIREON_API_URL=https://landing-api.pireon.dev/api -# Host port the container is published on. +# Host port the container is published on. Change it if something else already +# holds 3000 on the machine — the container always listens on 3000 internally. LANDING_PORT=3000 -# Only needed to *build* the image: `@pi/ui` comes from the private Nexus. -# Not read at runtime, and never baked into a layer — see Dockerfile. +# Needed only to *build* the image: `@pi/ui` comes from the private Nexus. +# Locally this is the value already sitting in ~/.npmrc: +# +# grep nexus.pireon.dev ~/.npmrc +# +# Never baked into a layer — it is mounted as a BuildKit secret. An empty value +# fails the build immediately with an explanation rather than a Nexus 401. NEXUS_TOKEN= diff --git a/.gitignore b/.gitignore index f712548..b6025d1 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,13 @@ Thumbs.db .nx/polygraph .angular + +# Secrets. `.env` holds NEXUS_TOKEN, which compose reads at build time, so it +# must never be committed — `.env.example` is the one that is. +.env +.env.* +!.env.example + +# A per-machine npmrc would carry a Nexus token; the committed one at the repo +# root deliberately holds none. +.npmrc.local diff --git a/Dockerfile b/Dockerfile index 6ba1d59..2cba65f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,14 +19,12 @@ COPY package.json package-lock.json .npmrc ./ # `@pi/ui` comes from the private Nexus, so the install needs a token — passed # as a BuildKit secret rather than a build ARG, which would be readable forever -# in the image history. The npmrc it lands in is written and removed inside one -# layer, so it leaves no trace even in this discarded stage. +# in the image history. The script checks the secret actually holds something +# before spending four minutes finding out from Nexus. +COPY tools/docker-install-deps.sh ./tools/ + RUN --mount=type=secret,id=nexus_token,required=true \ - sh -eu -c '\ - printf "//nexus.pireon.dev/repository/npm-releases/:_authToken=%s\n" \ - "$(cat /run/secrets/nexus_token)" > /tmp/npmrc; \ - NPM_CONFIG_USERCONFIG=/tmp/npmrc npm ci --no-audit --no-fund; \ - rm -f /tmp/npmrc' + sh tools/docker-install-deps.sh COPY nx.json tsconfig.base.json eslint.config.mjs ./ COPY landing ./landing diff --git a/README.md b/README.md index 82be6a6..1153e9e 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,25 @@ npm run e2e # Playwright end-to-end tests ## Deploying ```sh -NEXUS_TOKEN=… docker compose build +cp .env.example .env # then fill in NEXUS_TOKEN +docker compose build docker compose up -d ``` +Compose reads `.env` on its own, for the build token as well as the runtime +settings. `.env` is gitignored; `.env.example` is the one that is committed. If +you would rather not keep the token on disk: + +```sh +NEXUS_TOKEN=… docker compose build +``` + +An empty or missing token fails the build in under a second with a message +saying so. That check is there because the mount's own `required=true` only +asserts the secret was *declared* — an empty one passes it and then surfaces a +minute later as a Nexus 401 that reads like a credentials problem when it is +really a forgotten variable. + Two stages. The build one runs Nx and the Angular compiler over the whole workspace and is thrown away; only `dist/landing/analog` crosses into the runtime image, because Nitro vendors its own dependencies into that output — diff --git a/tools/docker-install-deps.sh b/tools/docker-install-deps.sh new file mode 100755 index 0000000..4f38ff5 --- /dev/null +++ b/tools/docker-install-deps.sh @@ -0,0 +1,52 @@ +#!/bin/sh +# Installs the workspace's dependencies inside the image. +# +# Split out of the Dockerfile so a failure prints this file's own message +# instead of BuildKit echoing a twenty-line inline shell twice. +set -eu + +TOKEN_FILE=/run/secrets/nexus_token +NEXUS_PATH=//nexus.pireon.dev/repository/npm-releases/ + +# `required=true` on the mount only asserts the secret was declared, not that it +# holds anything. An empty one sails past it and surfaces four hundred packages +# later as a Nexus 401 that reads like a credentials problem. +if [ ! -s "$TOKEN_FILE" ]; then + cat >&2 <<'MSG' + +ERROR: the nexus_token build secret is empty. + +@pi/ui is published to the private Nexus, so the install needs a token. + + compose: NEXUS_TOKEN=... docker compose build + or put NEXUS_TOKEN in .env, which compose reads on its own + docker: docker build --secret id=nexus_token,env=NEXUS_TOKEN . + +Locally the value is the one already in ~/.npmrc: + + grep nexus.pireon.dev ~/.npmrc + +MSG + exit 1 +fi + +# Written and removed inside a single layer, so it leaves no trace even in this +# stage — which is discarded anyway. +NPMRC=/tmp/npmrc +printf '%s:_authToken=%s\n' "$NEXUS_PATH" "$(cat "$TOKEN_FILE")" > "$NPMRC" + +if NPM_CONFIG_USERCONFIG="$NPMRC" npm ci --no-audit --no-fund; then + rm -f "$NPMRC" +else + status=$? + rm -f "$NPMRC" + + if [ "$status" -ne 0 ]; then + echo "" >&2 + echo "npm ci failed. If the error above is a 401 from nexus.pireon.dev," >&2 + echo "the token was present but is not valid for npm-releases." >&2 + echo "" >&2 + fi + + exit "$status" +fi