Sveltekit dockerized app cannot find package 'zod' imported from build

84 Views Asked by At

I recently containerized an sveltekit application. There are two main targets in my Dockerfile, one for dev and the other one for prod. The app uses zod (https://github.com/colinhacks/zod) for validation. It is only used in some DTO classes I've defined, for example:

import { z } from 'zod';
import {DTO} from "./DTO.js";

const categoryValidationSchema = z.object({
    name: z
       .string()
       .min(1)
       .trim(),
    parent: z
        .coerce
        .number({ required_error: 'Parent is required.' }),
    description: z
        .string()
        .optional()
});

/**
 * @class CategoryDTO
 */
export class CategoryDTO extends DTO {
    constructor() {
        super(categoryValidationSchema);
    }
}

And these classes are instantiated in some svelte kit's form actions (https://kit.svelte.dev/docs/form-actions).

When building and running dev the app works as expected. Now, when running prod I'm getting the following error:

dmc-web         | Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'zod' imported from /app/server/chunks/23-61add41d.js
dmc-web         |     at packageResolve (node:internal/modules/esm/resolve:844:9)
dmc-web         |     at moduleResolve (node:internal/modules/esm/resolve:901:20)
dmc-web         |     at defaultResolve (node:internal/modules/esm/resolve:1131:11)
dmc-web         |     at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:390:12)
dmc-web         |     at ModuleLoader.resolve (node:internal/modules/esm/loader:359:25)
dmc-web         |     at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:234:38)
dmc-web         |     at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:85:39)
dmc-web         |     at link (node:internal/modules/esm/module_job:84:36) {
dmc-web         |   code: 'ERR_MODULE_NOT_FOUND'
dmc-web         | }

Here's my Dockerfile:

# base stage
FROM node:21-alpine AS build

# Update and install the latest dependencies on docker base image
# Add non root user to the docker image and set the user
RUN apk update && apk upgrade && adduser -D svelteuser
USER svelteuser

WORKDIR /app

# Copy the sveltkit project content with proper permission for the user svelteuser
COPY --chown=svelteuser:svelteuser . /app

# install all the project npm dependencies and
# build the svelte project to generate the artifacts in build directory
RUN npm install && npm run build

# ----------------------------------------------------------
FROM build AS dev
# Set the Node environment to development to ensure all packages are installed
ENV NODE_ENV development
EXPOSE 3000
EXPOSE 5137

ENTRYPOINT npm run dev

# ---------------------

# We are using multi stage build process to keep the image size as small as possible
FROM node:21-alpine AS prod

# Update and install latest dependencies, add dumb-init package
# add and set non root user
RUN apk update && apk upgrade && apk add dumb-init && adduser -D svelteuser
USER svelteuser

# set work dir as app
WORKDIR /app

# copy the build directory to the /app directory of second stage
COPY --chown=svelteuser:svelteuser --from=build /app/build /app/package.json ./

# expose 8080 on container
EXPOSE 3000

# set app host and port and env as production
ENV HOST=0.0.0.0 PORT=3000 NODE_ENV=production

# start the app with dumb init to spawn the Node.js runtime process
# with signal support
CMD ["dumb-init","node","index.js"]

While doing some googling, I saw some people pointed out that "type": "module" is needed in package.json, but I had it already.

Here's my package.json:

{
    "name": "dundermifflin-ui",
    "version": "0.0.1",
    "private": true,
    "scripts": {
        "dev": "NODE_ENV=development vite dev --host",
        "build": "vite build",
        "preview": "vite preview",
        "test": "npm run test:integration && npm run test:unit",
        "lint": "prettier --plugin-search-dir . --check . && eslint .",
        "format": "prettier --plugin-search-dir . --write .",
        "test:integration": "playwright test",
        "test:unit": "vitest"
    },
    "devDependencies": {
        "@playwright/test": "^1.28.1",
        "@sveltejs/adapter-auto": "^2.0.0",
        "@sveltejs/kit": "^1.20.4",
        "autoprefixer": "^10.4.14",
        "daisyui": "^3.1.7",
        "eslint": "^8.28.0",
        "eslint-config-prettier": "^8.5.0",
        "eslint-plugin-svelte": "^2.30.0",
        "postcss": "^8.4.24",
        "prettier": "^2.8.0",
        "prettier-plugin-svelte": "^2.10.1",
        "svelte": "^4.0.0",
        "tailwindcss": "^3.3.2",
        "vite": "^4.3.6",
        "vitest": "^0.32.2"
    },
    "type": "module",
    "dependencies": {
        "@sveltejs/adapter-node": "^1.3.1",
        "html5-qrcode": "^2.3.8",
        "jsbarcode": "^3.11.6",
        "process": "^0.11.10",
        "svelte-select": "^5.6.1",
        "zod": "^3.21.4"
    }
}

Again, this issue is happening only for the prod build.

Any idea what's going one?

1

There are 1 best solutions below

0
VonC On BEST ANSWER

In your Dockerfile, during the prod build stage, you are only copying the build directory and package.json file from the build stage. However, you are not copying the node_modules directory or reinstalling the production dependencies. Since zod is a runtime dependency (listed under dependencies in package.json), it needs to be present in the production environment.

# previous stages remain the same 

# Production Stage
FROM node:21-alpine AS prod

# Update and install latest dependencies, add dumb-init package
# add and set non root user
RUN apk update && apk upgrade && apk add dumb-init && adduser -D svelteuser
USER svelteuser

WORKDIR /app

# Copy the package.json and package-lock.json (if exists)
COPY --chown=svelteuser:svelteuser package*.json ./


# Install only production dependencies   <===================
RUN npm install --only=production

# Copy the build directory from the build stage
COPY --chown=svelteuser:svelteuser --from=build /app/build ./build

EXPOSE 3000

ENV HOST=0.0.0.0 PORT=3000 NODE_ENV=production

CMD ["dumb-init","node","index.js"]

The node_modules directory is not explicitly copied in the suggested modification to the Dockerfile: copying node_modules from a development environment can bring in unnecessary or platform-specific files, leading to a bloated Docker image.

When you run npm install --only=production, npm looks at your package.json (and package-lock.json if present) and installs exactly what is needed for production. That step inherently creates the node_modules directory inside the container with all the production dependencies. By installing dependencies within the container, you make sure the production environment is isolated from any local development settings.

Docker can cache layers when building images. If package.json does not change often, Docker will use the cached layer of installed node_modules, speeding up the build process.

See "Dev Ops: Creating a Multi Stage Docker Container for Production" from Terence Tan for illustration.