我想在 VPS 上的生产环境中执行 Drizzle 迁移。
我无法在 Dockerfile 中运行 2 个命令
pnpm db:migrate:prod
和 pnpm start
。
# Where & how do I run `db:migrate:prod`?
CMD ["npm", "run", "start"]
我想在生产服务器上创建
users.prod.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
所以我自己想出了办法。我使用
run.sh
脚本来运行这两个脚本。
run.sh
同时包含pnpm db:migrate:prod
和pnpm start
,但我直接使用node server.js
,因为它是最佳实践,因为它可以很好地处理PID。
我还使用匿名卷来维护
node_modules
,由 https://michalzalecki.com/docker-compose-node/ 提供。我在 run.sh
脚本中安装依赖项,因此需要一些时间来启动容器。好像1分钟。但它是一次性的过程,所以我不介意,但我想优化。
无论如何,完整的解决方案位于https://github.com/deadcoder0904/easypanel-nextjs-sqlite/
我还使用了 chown 等权限,因为我了解到这是 Node.js 的最佳实践。
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
,您可以在链接的存储库中查看。