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"]