Я хочу выполнить моросящую миграцию на производстве на VPS.
Я не могу запустить две команды pnpm db:migrate:prod и pnpm start в Dockerfile.
# Where & how do I run `db:migrate:prod`?
CMD ["npm", "run", "start"]
Я хочу создать users.prod.sqlite на рабочем сервере, который сохранится, даже если контейнер выйдет из строя.
Стартовый репозиторий -> https://github.com/deadcoder0904/easypanel-nextjs-sqlite/tree/1fb34233283b1ff7b07b7f18e6973125ff96cbba
Как бы я это сделал?


Мне пришлось создать новый 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
Так что я понял это сам. Я использовал скрипт 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, который вы можете проверить в связанном репозитории.
Есть более простой способ:
// db.ts
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import * as schema from "../../schema";
const connectionString = process.env.DATABASE_URL || "";
export const pool = new Pool({
connectionString: connectionString,
});
export const db: NodePgDatabase<typeof schema> = drizzle(pool, { schema });
if (process.env.MIGRATE === "true") {
void migrate(db, { migrationsFolder: "./drizzle" });
}
# docker-compose.yml
services:
node-next:
environment:
- MIGRATE=true
Конечно, при выполнении миграции произойдет ошибка, но до тех пор, пока вы не переключите пользователей сразу на новое развертывание, все будет в порядке.