Fail the image build when the Nexus token is missing
`docker compose build` without NEXUS_TOKEN produced a Nexus 401 forty seconds in, worded as a credentials problem when it was really an unset variable. The mount's `required=true` asserts the secret was *declared*, not that it holds anything, so an empty one sailed past it and npm sent an empty `_authToken`. The install moves to tools/docker-install-deps.sh, which checks the secret before touching the network and fails in under a second with the three ways to supply it. It is a file rather than inline shell because BuildKit echoes an inline command twice on failure and buries the message. Also ignore .env. It carries NEXUS_TOKEN, compose reads it at build time, and it was not gitignored — .env.example stays tracked. Nothing had been committed: the only prior commit predates all of this. Verified: empty token fails in 0.2s with the message; .env and the environment variable both build; the image carries no trace of the token and serves the full page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
19
.env.example
19
.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=
|
||||
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -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
|
||||
|
||||
12
Dockerfile
12
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
|
||||
|
||||
17
README.md
17
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 —
|
||||
|
||||
52
tools/docker-install-deps.sh
Executable file
52
tools/docker-install-deps.sh
Executable file
@@ -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
|
||||
Reference in New Issue
Block a user