Skip to content

Build frontend dist in Docker image for production deployment#13

Merged
ChrisAdamsdevelopment merged 1 commit into
mainfrom
codex/update-dockerfile-for-frontend-dist-build
May 4, 2026
Merged

Build frontend dist in Docker image for production deployment#13
ChrisAdamsdevelopment merged 1 commit into
mainfrom
codex/update-dockerfile-for-frontend-dist-build

Conversation

@ChrisAdamsdevelopment
Copy link
Copy Markdown
Owner

@ChrisAdamsdevelopment ChrisAdamsdevelopment commented May 4, 2026

Motivation

  • The existing production image copied only server.js and production deps and therefore did not include the frontend dist/, causing the SPA UI to be absent in containers.
  • server.js serves dist/ with an SPA fallback for non-/api routes, so the image must contain dist/index.html and assets for correct production behavior.

Description

  • Reworked Dockerfile into a multi-stage build with a builder stage that runs npm ci and npm run build, and a runtime stage that installs runtime deps, installs perl for exiftool-vendored, creates a non-root user, copies server.js, and copies dist/ from the builder via COPY --from=builder /app/dist ./dist so the final image contains the built frontend.
  • The runtime stage sets NODE_ENV=production, keeps a non-root appuser, creates runtime directories /app/uploads and /data, exposes port 3001, and starts the app with npm start.
  • Updated README.md with docker build and docker run examples and a list of required production environment variables including JWT_SECRET, Stripe vars, GEMINI_API_KEY, FRONTEND_URL, DB_PATH, NODE_ENV, and optional REDIS_URL.
  • Kept dependency layout and application logic unchanged; no modifications were made to auth, Stripe, usage limits, server cleanse/browser cleanse, SEO generation, or metadata logic.

Testing

  • Ran npm install in the repo and it completed successfully.
  • Ran npm run build and the frontend produced dist/ (build completed and reported dist/index.html and asset files).
  • Attempted to run docker build -t spectracleanseai:test . but Docker is unavailable in this environment (docker: command not found), so the image build could not be executed here; the Dockerfile was inspected to verify dist/ is copied into the final stage.
  • Files changed: Dockerfile and README.md.

Codex Task

Summary by Sourcery

Introduce a multi-stage Docker-based production build that bundles the built frontend SPA into the runtime image and documents how to run it with required environment configuration.

Build:

  • Refactor Dockerfile into a multi-stage build that compiles the frontend and installs runtime-only dependencies, including Perl for exiftool, in a non-root Node 18 runtime image.

Documentation:

  • Add production Docker usage documentation with build/run examples and required environment variables.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 4, 2026

Reviewer's Guide

Multi-stage Dockerfile introduced to build the frontend and include its dist/ assets in the production image, plus README documentation for Docker-based production deployment and required env vars.

Flow diagram for Docker build and run process with environment and volume

flowchart TD
  start["Start"] --> build_cmd["Run docker build -t spectracleanseai:latest ."]

  build_cmd --> builder_stage["Builder stage
base image node:18-bookworm-slim
WORKDIR /app
npm ci
npm run build"]

  builder_stage --> runtime_stage["Runtime stage
base image node:18-bookworm-slim
ENV NODE_ENV=production
apt-get install perl
npm ci --omit=dev
COPY server.js
COPY dist from builder
mkdir /app/uploads /data
chown appuser:appgroup
USER appuser
EXPOSE 3001
CMD npm start"]

  runtime_stage --> image_built["Image spectracleanseai:latest built"]

  image_built --> run_cmd["Run docker run --rm -p 3001:3001 \
  -e NODE_ENV=production \
  -e JWT_SECRET \
  -e STRIPE_SECRET_KEY \
  -e STRIPE_WEBHOOK_SECRET \
  -e STRIPE_CREATOR_PRICE_ID \
  -e STRIPE_STUDIO_PRICE_ID \
  -e GEMINI_API_KEY \
  -e FRONTEND_URL \
  -e DB_PATH=/data/spectra.db \
  -v spectracleanse_data:/data \
  spectracleanseai:latest"]

  run_cmd --> container_running["Container running
server.js serves API and dist SPA on port 3001
SQLite uses /data via spectracleanse_data volume"]

  container_running --> end_node["Production app serving requests"]
Loading

File-Level Changes

Change Details Files
Introduce multi-stage Docker build that compiles the frontend and packages its dist output into the production runtime image.
  • Added a builder stage based on node:18-bookworm-slim that runs npm ci and npm run build to produce dist/.
  • Added a runtime stage based on node:18-bookworm-slim that sets NODE_ENV=production, installs only production dependencies, and uses npm start as the container command.
  • Installed perl via apt-get in the runtime stage to satisfy exiftool-vendored runtime requirements.
  • Created a non-root appuser/appgroup, set up /app/uploads and /data directories with appropriate ownership, and exposed port 3001.
  • Copied server.js and dist/ from the builder into the runtime image, ensuring the SPA assets are present in production containers.
Dockerfile
Document Docker-based production deployment workflow and required environment configuration.
  • Added build and run examples for the Docker production image, including recommended docker build and docker run commands.
  • Documented required production environment variables for auth, Stripe, Gemini, frontend URL, database path, and optional Redis URL.
  • Clarified the distinction between Stripe-based checkout in production and optional mock checkout for local development, with guidance to avoid mock checkout in production.
  • Added a note reminding not to commit real secrets to source control.
README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ChrisAdamsdevelopment ChrisAdamsdevelopment merged commit b77fe65 into main May 4, 2026
2 of 5 checks passed
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="Dockerfile" line_range="10-19" />
<code_context>

-# ExifTool requires Perl, which is not included in Alpine by default
-RUN apk add --no-cache perl
+COPY package.json package-lock.json ./
+RUN npm ci

-# Create a non-root user to run the process
-RUN addgroup -S appgroup && adduser -S appuser -G appgroup
+COPY . .
+RUN npm run build

+FROM node:18-bookworm-slim AS runtime
+ENV NODE_ENV=production
 WORKDIR /app

-# Install dependencies first so Docker can cache this layer
-COPY package*.json ./
-RUN npm ci --omit=dev
+# exiftool-vendored requires Perl at runtime
+RUN apt-get update \
+  && apt-get install -y --no-install-recommends perl \
+  && rm -rf /var/lib/apt/lists/*
+
+# Create non-root user
+RUN groupadd --system appgroup && useradd --system --gid appgroup appuser
+
+COPY package.json package-lock.json ./
+RUN npm ci --omit=dev \
+  && npm cache clean --force

</code_context>
<issue_to_address>
**suggestion (performance):** Consider reusing node_modules from the builder stage to avoid a second full npm install

Dependencies are currently installed twice (`npm ci` in the builder and `npm ci --omit=dev` in the runtime). To reduce build time and network usage, you could copy `node_modules` from the builder into the runtime image and then run `npm prune --production` (or equivalent) there, preserving the multi-stage setup without a second full install.

Suggested implementation:

```
# SpectraCleanse AI – Production Dockerfile (builds frontend + backend runtime)
# ─────────────────────────────────────────────────────────────────────────────

FROM node:18-bookworm-slim AS builder
WORKDIR /app

# Install full dependency tree (including devDependencies) for build
COPY package.json package-lock.json ./
RUN npm ci

# Copy source and build
COPY . .
RUN npm run build

```

To fully implement reusing `node_modules` from the builder in the runtime image, you should also:

1. In the `runtime` stage (the `FROM node:18-bookworm-slim AS runtime` section), replace the second `npm ci --omit=dev` with copying `node_modules` from the builder and pruning dev dependencies, e.g.:

   ```dockerfile
   # Instead of:
   COPY package.json package-lock.json ./
   RUN npm ci --omit=dev \
     && npm cache clean --force

   # Use:
   COPY package.json package-lock.json ./
   COPY --from=builder /app/node_modules ./node_modules

   RUN npm prune --production \
     && npm cache clean --force
   ```

2. Ensure you copy the built artifacts from the builder into the runtime image (if not already present), for example:

   ```dockerfile
   COPY --from=builder /app/dist ./dist
   ```

3. Keep the existing Perl installation and non-root user setup in the runtime stage as-is, so that `exiftool-vendored` continues to work and the container still runs as `appuser`.

Adjust paths like `/app/dist` or the final `CMD` to match your existing build output and startup command.
</issue_to_address>

### Comment 2
<location path="README.md" line_range="81" />
<code_context>
+### Required production environment variables
+
+- `NODE_ENV=production`
+- `JWT_SECRET`
+- `STRIPE_SECRET_KEY`
+- `STRIPE_WEBHOOK_SECRET`
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Clarify that `JWT_SECRET` should be a strong, unique secret in production.

Add a brief note to this bullet that `JWT_SECRET` must be a strong, unique value and not reused across environments to avoid weak production configurations.

```suggestion
- `JWT_SECRET` – a strong, unique secret value; **do not** reuse across environments (e.g., dev/staging/production)
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread Dockerfile
Comment on lines +10 to 19
COPY package.json package-lock.json ./
RUN npm ci

# Create a non-root user to run the process
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY . .
RUN npm run build

FROM node:18-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Consider reusing node_modules from the builder stage to avoid a second full npm install

Dependencies are currently installed twice (npm ci in the builder and npm ci --omit=dev in the runtime). To reduce build time and network usage, you could copy node_modules from the builder into the runtime image and then run npm prune --production (or equivalent) there, preserving the multi-stage setup without a second full install.

Suggested implementation:

# SpectraCleanse AI – Production Dockerfile (builds frontend + backend runtime)
# ─────────────────────────────────────────────────────────────────────────────

FROM node:18-bookworm-slim AS builder
WORKDIR /app

# Install full dependency tree (including devDependencies) for build
COPY package.json package-lock.json ./
RUN npm ci

# Copy source and build
COPY . .
RUN npm run build

To fully implement reusing node_modules from the builder in the runtime image, you should also:

  1. In the runtime stage (the FROM node:18-bookworm-slim AS runtime section), replace the second npm ci --omit=dev with copying node_modules from the builder and pruning dev dependencies, e.g.:

    # Instead of:
    COPY package.json package-lock.json ./
    RUN npm ci --omit=dev \
      && npm cache clean --force
    
    # Use:
    COPY package.json package-lock.json ./
    COPY --from=builder /app/node_modules ./node_modules
    
    RUN npm prune --production \
      && npm cache clean --force
  2. Ensure you copy the built artifacts from the builder into the runtime image (if not already present), for example:

    COPY --from=builder /app/dist ./dist
  3. Keep the existing Perl installation and non-root user setup in the runtime stage as-is, so that exiftool-vendored continues to work and the container still runs as appuser.

Adjust paths like /app/dist or the final CMD to match your existing build output and startup command.

Comment thread README.md
### Required production environment variables

- `NODE_ENV=production`
- `JWT_SECRET`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 suggestion (security): Clarify that JWT_SECRET should be a strong, unique secret in production.

Add a brief note to this bullet that JWT_SECRET must be a strong, unique value and not reused across environments to avoid weak production configurations.

Suggested change
- `JWT_SECRET`
- `JWT_SECRET` – a strong, unique secret value; **do not** reuse across environments (e.g., dev/staging/production)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant