# syntax=docker/dockerfile:1 # # YourPHR: the Angular app and the TypeScript server, one image (yourphr#587, yourphr#652). # # THE FRONTEND IS BUILT HERE, from frontend/ in this repository. It used to be copied out of the # released Go image, because the two stacks lived in two repositories and the spike had no frontend # of its own. They are one repository now (yourphr#650), so the UI and the server that serves it are # built from the same commit — which is the thing that arrangement could not give. # # MULTI-ARCH without native runners: the TypeScript compiles on the BUILD platform (its output is # arch-independent), and the only native dependency, better-sqlite3-multiple-ciphers, ships linux # x64 AND arm64 prebuilds, so the per-arch stages download rather than compile. No QEMU compile. # --- the Angular app -------------------------------------------------------------------------- FROM --platform=$BUILDPLATFORM node:24-bookworm-slim AS frontend WORKDIR /build/frontend COPY frontend/package.json frontend/yarn.lock ./ RUN yarn install --frozen-lockfile --network-timeout 600000 COPY frontend ./ # Writes to /build/dist/web — one dist/, all TypeScript, and the Angular build CLEARS its output # directory, which is why the two halves have their own subdirectories. # # `-c prod` IS LOAD-BEARING (yourphr#673). angular.json declares no defaultConfiguration, so a bare # `ng build` applies no fileReplacements and ships src/environments/environment.ts — the DEVELOPMENT # file, with production:false (enableProdMode never runs) and environment_name "sandbox". 3.0.0 and # 3.1.0 both shipped it, which is how a production PHR came to display "sandbox-3.1.0" and to tell # patients on the sources page that it "cannot access real patient information". # # The Go image built with `--configuration ${YOURPHR_ENV}`; that flag was lost when the frontend # build moved into this repository (yourphr#652). It is `prod` unconditionally now rather than a # build argument: one image serves every instance, and what an instance IS is runtime configuration # (yourphr.web.environment-name), never something compiled in. RUN yarn build -- -c prod # The tooth. `outputHashing: "all"` lives in the CONFIGURATIONS, not the base options, so hashed # filenames are the structural signature that a configuration was applied at all. Asserting on that # rather than on a string inside the bundle is deliberate: a marker string is a guard that rots # silently the day someone edits the environment file, and this failure mode is already one that # looked exactly like success. RUN test ! -e /build/dist/web/main.js \ && ls /build/dist/web/main.*.js > /dev/null \ || (echo "REFUSED: the Angular output is unhashed — 'ng build' ran with no configuration, so it has shipped environment.ts (development). See yourphr#673." >&2; exit 1) # --- compile TypeScript once, on the build platform ------------------------------------------- FROM --platform=$BUILDPLATFORM node:24-bookworm-slim AS build WORKDIR /build COPY package.json package-lock.json tsconfig.json tsconfig.build.json ./ # --ignore-scripts: the repo's `prepare` hook wires git hooks, which has no business in an image. RUN npm ci --ignore-scripts COPY src ./src RUN npm run build # The demo's baked-in baseline (yourphr#645): the databases a public demo resets itself to, built # HERE so what ships is reproducible and nobody hand-curates a database strangers will read. # Everything in it is synthetic, generated by the same fake provider the harnesses use, and it # carries no working credential — both demo passwords are provisioned at startup. SQLite files are # architecture-independent, so building on the build platform is correct. COPY scripts ./scripts COPY config ./config RUN npx tsx scripts/build-demo-baseline.ts --out /build/baseline # --- production dependencies for the TARGET platform ------------------------------------------ FROM node:24-bookworm-slim AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force # --- the runtime -------------------------------------------------------------------------------- FROM node:24-bookworm-slim ENV NODE_ENV=production WORKDIR /opt/yourphr COPY --from=deps /app/node_modules ./node_modules COPY --from=build /build/dist/server ./dist/server COPY package.json ./ # The shipped defaults are part of the product (yourphr#621): the process refuses to boot without # them, and WORKDIR is where FileConfigProvider looks. Instance overrides live under ./data. COPY config ./config # Inert on every instance that is not a demo: the reset needs demo.enabled AND reset-on-restart AND # this directory, and then still has to prove the database belongs to a demo (yourphr#645). COPY --from=build /build/baseline ./baseline COPY --from=frontend /build/dist/web ./web RUN mkdir -p /opt/yourphr/data && chown -R node:node /opt/yourphr USER node # Bootstrap only (yourphr#472): where the data lives, where the UI is, which port. Settings live in # the config store under the data directory. Secrets (YOURPHR_DATABASE_ENCRYPTION_KEY, # YOURPHR_BACKUP_ENCRYPTION_KEY) come from the orchestrator, never from an image. # Names derive from the configuration keys, which carry the yourphr. prefix (yourphr#627). The # pre-#627 SPIKE_* names are still accepted with a warning, so a deployment whose manifests have # not been updated keeps booting; that fallback goes at the cut-over (yourphr#588). Secrets and any # other bootstrap value belong in /.env on the mounted volume (yourphr#630), # never in this image. ENV YOURPHR_FAST_STORAGE=/opt/yourphr/data \ YOURPHR_SLOW_STORAGE=/opt/yourphr/data \ YOURPHR_WEB_STATIC_DIR=/opt/yourphr/web \ YOURPHR_WEB_LISTEN_PORT=8080 EXPOSE 8080 VOLUME ["/opt/yourphr/data"] # ENTRYPOINT plus a default CMD, not CMD alone (yourphr#654). Words after the image name REPLACE # CMD and leave ENTRYPOINT in place, so `docker run … migrate --go …` — what the upgrade # guide has always told self-hosters to run — becomes `node dist/server/main.js migrate --go …`. # With CMD alone those words replaced the whole command and docker looked for a binary called # `migrate`, which is why the documented upgrade could not be performed from the image. # # `start` is the default, so an argument-less `docker run` and every existing Deployment (neither # yourphr nor demo-yourphr sets command: or args:) behave exactly as before. A shell for debugging # now needs --entrypoint, which is the ordinary cost of a container that has more than one job. ENTRYPOINT ["node", "dist/server/main.js"] CMD ["start"]