FROM node:20-bookworm-slim AS frontend-build
WORKDIR /app/frontend
COPY frontend/package.json ./
RUN npm install
COPY frontend/ ./
RUN npm run build

FROM node:20-bookworm-slim AS backend-build
WORKDIR /app/backend
COPY backend/package.json ./
RUN npm install
COPY backend/ ./
# Only this stage ever sees .git, and the final image below copies just `dist`,
# so no repository history reaches the deployed container. The build reads the
# commit out of it directly rather than shelling out to git, because this base
# image has no git binary and adding an apt layer so the image can print seven
# characters is a poor trade. See #233.
#
# There is no .dockerignore, so .git is in the build context here and in
# Portainer, which clones the repository before building.
COPY .git ./.git
# writeBuildInfo runs against the compiled output, so it has to follow tsc. It
# warns rather than fails when .git is unreadable: a deploy must never be
# stopped by the thing whose only job is to say which deploy it is.
RUN npm run build && node dist/writeBuildInfo.js

FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=backend-build /app/backend/package.json ./
RUN npm install --omit=dev
COPY --from=backend-build /app/backend/dist ./dist
COPY --from=backend-build /app/backend/migrate.js ./migrate.js
COPY --from=backend-build /app/backend/migrations ./migrations
COPY --from=frontend-build /app/frontend/dist ./public
ENV NODE_ENV=production
EXPOSE 3000
# Migrations run before the app serves, so deployed code can never be ahead of
# the database schema. Previously this was a separate, manual
# `docker exec ... node migrate.js up` that was easy to forget — and forgetting
# it meant every item query referenced tables that did not exist yet, which
# surfaced as an empty storefront rather than an error.
#
# `&&` matters: migrate.js exits non-zero on failure, so a bad migration stops
# the container instead of letting it serve against a half-migrated schema.
CMD ["sh", "-c", "node migrate.js up && node dist/server.js"]