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?
In your Dockerfile, during the
prodbuild stage, you are only copying thebuilddirectory andpackage.jsonfile from thebuildstage. However, you are not copying thenode_modulesdirectory or reinstalling the production dependencies. Sincezodis a runtime dependency (listed underdependenciesinpackage.json), it needs to be present in the production environment.The
node_modulesdirectory is not explicitly copied in the suggested modification to theDockerfile: copyingnode_modulesfrom 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 yourpackage.json(andpackage-lock.jsonif present) and installs exactly what is needed for production. That step inherently creates thenode_modulesdirectory 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.jsondoes not change often, Docker will use the cached layer of installednode_modules, speeding up the build process.See "Dev Ops: Creating a Multi Stage Docker Container for Production" from Terence Tan for illustration.