Я устанавливаю все свои зависимости в Dockerfile, используя npm ci ниже.
FROM node:20-alpine AS base
# 1. Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
COPY /src/app/db/migrations ./migrations
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \
else echo "Lockfile not found." && exit 1; \
fi
# 2. Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# This will do the trick, use the corresponding env file for each environment.
COPY .env.production .env.production
RUN mkdir -p /data
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Create /data/users.prod.sqlite using Volume Mount
# RUN npm run db:migrate:prod
RUN npm run build
# 3. Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=builder /app/public ./public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Move the drizzle directory to the runtime image
COPY --from=builder --chown=nextjs:nodejs /app/src/app/db/migrations ./migrations
# Move the run script and litestream config to the runtime image
COPY --from=builder /app/scripts/drizzle-migrate.mjs ./scripts/drizzle-migrate.mjs
COPY --from=builder /app/scripts/init.sh ./init.sh
COPY --from=builder /app/scripts/run.sh ./run.sh
RUN chmod +x init.sh
RUN chmod +x run.sh
USER nextjs
EXPOSE 3000
CMD ["sh", "run.sh"]
#!/bin/bash
set -e
# npm run db:migrate:prod & PID=$!
# Wait for migration to finish
# wait $PID
echo "Starting production server..."
node server.js & PID=$!
wait $PID
Когда я делаю docker-compose up, я получаю эту ошибку:
Ошибка [ERR_MODULE_NOT_FOUND]: невозможно найти пакет «dizzle-orm», импортированный из /app/scripts/dizzle-migrate.mjs.
Мой package.json содержит эти 2 скрипта:
"db:migrate:prod": "node --env-file .env.production ./scripts/drizzle-migrate.mjs"
"start": "next start",
Я также проверил docker run -it easypanel-nextjs npm list и получил это:
npm ERR! code ELSPROBLEMS
npm ERR! missing: @epic-web/remember@^1.0.2, required by [email protected]
npm ERR! missing: @t3-oss/env-nextjs@^0.9.2, required by [email protected]
npm ERR! missing: drizzle-orm@^0.29.3, required by [email protected]
npm ERR! missing: jiti@^1.21.0, required by [email protected]
npm ERR! missing: std-env@^3.7.0, required by [email protected]
npm ERR! missing: tsx@^4.7.1, required by [email protected]
npm ERR! missing: zod@^3.22.4, required by [email protected]
[email protected] /app
+-- UNMET DEPENDENCY @epic-web/remember@^1.0.2
+-- UNMET DEPENDENCY @t3-oss/env-nextjs@^0.9.2
+-- [email protected] -> ./node_modules/.pnpm/[email protected]/node_modules/better-sqlite3
+-- UNMET DEPENDENCY drizzle-orm@^0.29.3
+-- UNMET DEPENDENCY jiti@^1.21.0
+-- [email protected] -> ./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next
+-- [email protected] -> ./node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom
+-- [email protected] -> ./node_modules/.pnpm/[email protected]/node_modules/react
+-- UNMET DEPENDENCY std-env@^3.7.0
+-- UNMET DEPENDENCY tsx@^4.7.1
`-- UNMET DEPENDENCY zod@^3.22.4
Как мне решить эту ошибку?
Я даже пробовал использовать анонимные модули, но и они не сработали.
version: '3.8'
services:
web:
build:
context: .
dockerfile: docker/web/Dockerfile
# depends_on:
# db:
# condition: service_healthy
# redis:
# condition: service_started
image: easypanel-nextjs
container_name: nextjs-sqlite
env_file:
- .env.production
ports:
- 3000:3000
volumes:
- ./data:/data
- /app/node_modules
# migration:
# build:
# context: .
# dockerfile: docker/migrations/Dockerfile
# image: easypanel-nextjs
# depends_on:
# web:
# condition: service_completed_successfully
Мой контейнер останавливается с этой ошибкой:
> [email protected] db:migrate:prod
> node --env-file .env.production ./scripts/drizzle-migrate.mjs
node:internal/modules/esm/resolve:853
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'drizzle-orm' imported from /app/scripts/drizzle-migrate.mjs
at packageResolve (node:internal/modules/esm/resolve:853:9)
at moduleResolve (node:internal/modules/esm/resolve:910:20)
at defaultResolve (node:internal/modules/esm/resolve:1130:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:396:12)
at ModuleLoader.resolve (node:internal/modules/esm/loader:365:25)
at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:240:38)
at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:85:39)
at link (node:internal/modules/esm/module_job:84:36) {
code: 'ERR_MODULE_NOT_FOUND'
}
Node.js v20.11.1
Когда я пытаюсь войти внутрь остановленного контейнера, используя docker run -it easypanel-nextjs sh и do ls node_modules, я получаю следующее:
/app $ ls node_modules/
better-sqlite3 next react react-dom
Всего 4 зависимости. У моего package.json много зависимостей:
{
"name": "easypanel-nextjs-sqlite",
"type": "module",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"turbo": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint",
"knip": "knip",
"clean": "rimraf .next",
"db:push": "drizzle-kit push:sqlite",
"db:generate": "drizzle-kit generate:sqlite",
"db:migrate": "node --env-file .env.development ./scripts/drizzle-migrate.mjs",
"db:seed": "node --import tsx --env-file .env.development ./scripts/seed/insert.ts",
"db:delete": "node --import tsx --env-file .env.development ./scripts/seed/delete.ts",
"db:migrate:prod": "node --env-file .env.production ./scripts/drizzle-migrate.mjs"
},
"dependencies": {
"@epic-web/remember": "^1.0.2",
"@t3-oss/env-nextjs": "^0.9.2",
"better-sqlite3": "^9.4.1",
"drizzle-orm": "^0.29.3",
"jiti": "^1.21.0",
"next": "14.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"std-env": "^3.7.0",
"tsx": "^4.7.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.9",
"@types/node": "^20.11.19",
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"drizzle-kit": "^0.20.14",
"knip": "^5.0.1",
"rimraf": "^5.0.5",
"typescript": "^5.3.3"
}
}
В чем проблема?
У меня есть полная репродукция -> https://github.com/deadcoder0904/easypanel-nextjs-sqlite
спасибо @DavidMaze, но я понял это только сейчас, спустя неделю. ответ ниже. я думаю, это была комбинация chown (правильные разрешения для node_modules), анонимных томов, установки всех производственных зависимостей, а затем их удаления с помощью --prune и множества других вещей. Мне бы очень хотелось, если бы вы ответили, как я могу сделать make start-production быстрее, как сейчас pnpm install и это занимает как минимум 60 секунд. Я упомянул вопрос ниже в конце ответа.





Мне пришлось создать новый package.json только с зависимостями Drizzle Migration.
{
"name": "scripts",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"better-sqlite3": "^9.4.3",
"drizzle-orm": "^0.29.4",
"std-env": "^3.7.0"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Я также удалил node_modules из финального изображения, так как он больше не нужен. Это значительно уменьшило размер изображения до 175 МБ.
Я удалил блок prod-dependencies FROM из Dockerfile, чтобы немного упростить его.
И скопировал scripts/package.json, чтобы установить из него зависимости.
FROM node:20-alpine AS base
# mostly inspired from https://github.com/BretFisher/node-docker-good-defaults/blob/main/Dockerfile & https://github.com/remix-run/example-trellix/blob/main/Dockerfile
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
RUN corepack enable && corepack prepare [email protected] --activate
# set the store dir to a folder that is not in the project
RUN pnpm config set store-dir ~/.pnpm-store
RUN pnpm fetch
# 1. Install all dependencies including dev dependencies
FROM base AS deps
# Root user is implicit so you don't have to actually specify it. From https://stackoverflow.com/a/45553149/6141587
# USER root
USER node
# WORKDIR now sets correct permissions if you set USER first so `USER node` has permissions on `/app` directory
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY --chown=node:node package.json pnpm-lock.yaml* ./
COPY --chown=node:node /src/app/db/migrations ./migrations
USER root
RUN pnpm install --frozen-lockfile --prefer-offline
# 2. Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --chown=node:node . .
# This will do the trick, use the corresponding env file for each environment.
COPY --chown=node:node .env.production .env.production
# Copied from https://stackoverflow.com/a/69867550/6141587
USER root
# Give /data directory correct permissions otherwise WAL mode won't work. It means you can't have 2 users writing to the database at the same time without this line as *.sqlite-wal & *.sqlite-shm are automatically created & deleted when *.sqlite is busy.
RUN mkdir -p /data && chown -R node:node /data
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build
# 3. Production image, copy all the files and run next
FROM base AS runner
USER node
WORKDIR /app
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME '0.0.0.0'
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder --chown=node:node /app/public ./public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
# Move the drizzle directory to the runtime image
COPY --from=builder --chown=node:node /app/src/app/db/migrations ./migrations
# Move the run script and litestream config to the runtime image
COPY --from=builder --chown=node:node /app/scripts/drizzle-migrate.mjs ./scripts/drizzle-migrate.mjs
COPY --from=builder --chown=node:node /app/scripts/package.json ./scripts/package.json
COPY --from=builder --chown=node:node /app/scripts/pnpm-lock.yaml ./scripts/pnpm-lock.yaml
COPY --from=builder --chown=node:node /app/scripts/run.sh ./run.sh
RUN chmod +x run.sh
CMD ["sh", "run.sh"]
Здесь я устанавливаю только необходимые для миграции зависимости.
#!/bin/bash
set -e
# Only install dependencies for drizzle migration. Those are not bundled via `next build` as its optimized to only install dependencies that are used`
echo "Installing production dependencies"
cd scripts
pnpm config set store-dir ~/.pnpm-store
pnpm fetch
pnpm install --prod --prefer-offline
cd ..
echo "Creating '/data/users.prod.sqlite' using bind volume mount"
pnpm run db:migrate:prod & PID=$!
# Wait for migration to finish
wait $PID
echo "Starting production server..."
node server.js & PID=$!
wait $PID
Я удалил pnpm install из run.sh, который медленно загружал контейнер (docker compose up). Итак, 1-минутное ожидание прошло. И на заключительном этапе я скопировал node_modules с производственными зависимостями, в результате чего размер моего образа увеличился со 198 МБ до 611 МБ, но меня это устраивает.
Затем я попытался добавить режим SQLite WAL, что привело к потере данных из-за проблем с сетью. Не знаю, почему это не работает должным образом, но это не вина Докера и неправильный синтаксис моего тома. Итак, я закомментировал режим WAL, и теперь все работает нормально.
Новое решение использует лучшие практики безопасности в Node.js с использованием непривилегированных пользователей. И решение намного чище и проще для понимания.
version: '3.8'
services:
web:
image: easypanel-nextjs:0.0.1
build:
context: .
dockerfile: Dockerfile
container_name: nextjs-sqlite
env_file:
- .env.production
ports:
- 3000:3000
volumes:
- ./data:/data
FROM node:20-alpine AS base
# mostly inspired from https://github.com/BretFisher/node-docker-good-defaults/blob/main/Dockerfile & https://github.com/remix-run/example-trellix/blob/main/Dockerfile
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
RUN corepack enable && corepack prepare [email protected] --activate
# set the store dir to a folder that is not in the project
RUN pnpm config set store-dir ~/.pnpm-store
RUN pnpm fetch
# 1. Install all dependencies including dev dependencies
FROM base AS deps
# Root user is implicit so you don't have to actually specify it. From https://stackoverflow.com/a/45553149/6141587
# USER root
USER node
# WORKDIR now sets correct permissions if you set USER first so `USER node` has permissions on `/app` directory
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY --chown=node:node package.json pnpm-lock.yaml* ./
COPY --chown=node:node /src/app/db/migrations ./migrations
USER root
RUN pnpm install
# 2. Setup production node_modules
FROM base as production-deps
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --chown=node:node package.json pnpm-lock.yaml* ./
RUN pnpm prune --prod
# 3. Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --chown=node:node . .
# This will do the trick, use the corresponding env file for each environment.
COPY --chown=node:node .env.production .env.production
# Copied from https://stackoverflow.com/a/69867550/6141587
USER root
# Give /data directory correct permissions otherwise WAL mode won't work. It means you can't have 2 users writing to the database at the same time without this line as *.sqlite-wal & *.sqlite-shm are automatically created & deleted when *.sqlite is busy.
RUN mkdir -p /data && chown -R node:node /data
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build
# 3. Production image, copy all the files and run next
FROM base AS runner
USER node
WORKDIR /app
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME '0.0.0.0'
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder --chown=node:node /app/public ./public
COPY --from=production-deps --chown=node:node /app/node_modules ./node_modules
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
# Move the drizzle directory to the runtime image
COPY --from=builder --chown=node:node /app/src/app/db/migrations ./migrations
# Move the run script and litestream config to the runtime image
COPY --from=builder --chown=node:node /app/scripts/drizzle-migrate.mjs ./scripts/drizzle-migrate.mjs
COPY --from=builder --chown=node:node /app/scripts/run.sh ./run.sh
RUN chmod +x run.sh
CMD ["sh", "run.sh"]
#!/bin/bash
set -e
echo "Creating '/data/users.prod.sqlite' using bind volume mount"
pnpm run db:migrate:prod & PID=$!
# Wait for migration to finish
wait $PID
echo "Starting production server..."
node server.js & PID=$!
wait $PID
Причиной этого было то, что моего node_modules не существовало, когда я зашел в run.sh, или, по крайней мере, там было только несколько папок, о которых я упомянул выше, поэтому я решил, что мне следует npm install их run.sh, и это то, что я сделал.
На самом деле я перешел на pnpm, так как думал, что это будет быстро. Не знаю, действительно ли он быстрый в моем докере, но локально он определенно быстрый.
Я также использовал такие разрешения, как chown, когда узнал, что это лучшая практика Node.js.
И, наконец, я использовал node_modules в качестве анонимных томов, любезно предоставленных https://michalzalecki.com/docker-compose-node/
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile
image: easypanel-nextjs
container_name: nextjs-sqlite
env_file:
- .env.production
ports:
- 3000:3000
volumes:
- ./data:/data
- /app/node_modules
FROM node:20-alpine AS base
# mostly inspired from https://github.com/BretFisher/node-docker-good-defaults/blob/main/Dockerfile & https://github.com/remix-run/example-trellix/blob/main/Dockerfile
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
RUN corepack enable && corepack prepare [email protected] --activate
# set the store dir to a folder that is not in the project
RUN pnpm config set store-dir ~/.pnpm-store
RUN pnpm fetch
# 1. Install all dependencies including dev dependencies
FROM base AS deps
# Root user is implicit so you don't have to actually specify it. From https://stackoverflow.com/a/45553149/6141587
# USER root
USER node
# WORKDIR now sets correct permissions if you set USER first so `USER node` has permissions on `/app` directory
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY --chown=node:node package.json pnpm-lock.yaml* ./
COPY --chown=node:node /src/app/db/migrations ./migrations
USER root
RUN pnpm install
USER node
# 2. Setup production node_modules
FROM base as production-deps
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --chown=node:node package.json pnpm-lock.yaml* ./
RUN pnpm prune --prod
# 3. Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --chown=node:node . .
# This will do the trick, use the corresponding env file for each environment.
COPY --chown=node:node .env.production .env.production
RUN mkdir -p /data
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build
# 3. Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder --chown=node:node /app/public ./public
COPY --from=production-deps --chown=node:node /app/node_modules ./node_modules
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
# Move the drizzle directory to the runtime image
COPY --from=builder --chown=node:node /app/src/app/db/migrations ./migrations
# Move the run script and litestream config to the runtime image
COPY --from=builder --chown=node:node /app/scripts/drizzle-migrate.mjs ./scripts/drizzle-migrate.mjs
COPY --from=builder --chown=node:node /app/scripts/run.sh ./run.sh
RUN chmod +x run.sh
EXPOSE 3000
CMD ["sh", "run.sh"]
#!/bin/bash
set -e
echo "Installing dependencies using pnpm..."
pnpm install & PID=$!
wait $PID
echo "Creating 'data/users.prod.sqlite' using bind volume mount"
pnpm run db:migrate:prod & PID=$!
# Wait for migration to finish
wait $PID
echo "Starting production server..."
node server.js & PID=$!
wait $PID
Полная рабочая версия находится по адресу https://github.com/deadcoder0904/easypanel-nextjs-sqlite/
Единственное предостережение сейчас: когда я запускаю контейнер, это требует времени, потому что он работает pnpm install.
Мне бы хотелось придумать, как сделать так, чтобы этого не приходилось pnpm install каждый раз делать make start-production в моем проекте. Я использую Makefile, который вы можете проверить в связанном репозитории.
Вам не нужно запускать pnpm install в сценарии run.sh; вы уже устанавливаете пакеты в свой Dockerfile. Обязательно удалите строку volumes:, которая скрывает дерево node_modules изображения, чтобы вы могли использовать библиотеки, встроенные в изображение.
спасибо @DavidMaze, изначально я использовал его, потому что получил ошибку, которую опубликовал cannot find package 'drizzle-orm', думаю, после того, как я скопировал node_modules, содержащий зависимости продукта на этапе runner, исправил ее. Итак, я попробовал ваше предложение и думаю, что оно работает хорошо. пожалуйста, подтвердите, что я говорю правильно, хотя размер образа увеличился со 198 МБ до 611 МБ из-за производственных зависимостей. в любом случае, чтобы уменьшить это вообще?
@DavidMaze Я попробовал ваше решение, но по какой-то причине моя база данных не синхронизируется с использованием монтирования тома. например, я удалил папку ./data локально, и она была удалена из /data места, но мое приложение по-прежнему работает без ошибок, а данные все еще там. я не смог найти файл *.sqlite где-либо еще в моем контейнере. понятия не имею, где хранится .sqlite и использует ли он другое изображение. у меня есть 2 изображения (docker images), но работает только 1 контейнер (docker ps). Есть идеи, почему это происходит?
@DavidMaze, поэтому я понял, что ошибка, с которой я столкнулся, была связана не с докером и не с тем, как я использовал тома, а с sqlite. Оказывается, режим wal в sqlite не работает с сетевыми файловыми системами, что приводит к потере данных. все работает нормально, если я отключу режим Wal, так что, думаю, я сделал именно это. сейчас обновлю ответ, спасибо!
Помогает ли удаление блока
volumes:в файле Compose? Вашеnode_modulesдерево взято из анонимного тома, а не из изображения, что может привести к подобным проблемам.