我使用下面的
npm ci
在 Dockerfile 中安装所有依赖项。
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]:找不到从 /app/scripts/drizzle-migrate.mjs 导入的包“drizzle-orm”
我的
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
进入停止的容器并执行 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
我从
pnpm install
中删除了 run.sh
,它启动容器 (docker compose up
) 的速度很慢。于是1分钟的等待就消失了。我在最后阶段复制了带有生产依赖项的 node_modules
,这使我的图像从 198mb 变为 611mb,但这对我来说没关系。
然后我尝试添加 SQLite WAL 模式,结果由于网络问题导致数据丢失。我不知道为什么它不能正常工作,但这既不是 Docker 的错,也不是我的 Volume 语法错误。所以我注释掉了 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
,因为我认为它会很快。我不知道它在我的 docker 上是否真的很快,但在本地肯定很快。
我还使用了 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
,您可以在链接的存储库中查看。