diff --git a/.dockerignore b/.dockerignore index fbe4336..aa00244 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,7 +15,6 @@ dist pgdata redis-data logs -drizzle # Environment .env diff --git a/Dockerfile b/Dockerfile index 3727263..a88409b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,6 +61,7 @@ COPY --from=builder --chown=node:node /usr/src/app/docker-entrypoint.sh ./docker COPY --from=builder --chown=node:node /usr/src/app/.env.example ./.env.example COPY --from=builder --chown=node:node /usr/src/app/exchanges ./exchanges COPY --from=builder --chown=node:node /usr/src/app/drizzle ./drizzle +COPY --from=builder --chown=node:node /usr/src/app/scripts ./scripts RUN chmod +x /usr/src/app/docker-entrypoint.sh diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 858043a..2c19eab 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -67,5 +67,13 @@ until pg_isready -h "${POSTGRESQL_HOST:-postgres}" -p "${POSTGRESQL_PORT:-5432}" sleep 1 done -echo "Postgres is available. Starting app..." +echo "Postgres is available. Running database migrations..." +node /usr/src/app/scripts/migrate.js + +if [ $? -ne 0 ]; then + echo "ERROR: Database migrations failed!" + exit 1 +fi + +echo "Database migrations completed successfully. Starting app..." exec "$@" \ No newline at end of file diff --git a/package.json b/package.json index 951b14a..85f0ee3 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "drizzle:generate": "npx drizzle-kit generate", "drizzle:generate:custom": "npx drizzle-kit generate --custom", - "drizzle:migrate": "npx drizzle-kit migrate", + "drizzle:migrate": "node scripts/migrate.js", "drizzle:push": "npx drizzle-kit push", "drizzle:studio": "npx drizzle-kit studio", "start": "node --env-file=.env build" diff --git a/readme.md b/readme.md index 0711829..8b8d973 100644 --- a/readme.md +++ b/readme.md @@ -38,3 +38,20 @@ - vite (빌드 도구) - Zod (데이터 검증) +--- +## Database Migrations / 데이터베이스 마이그레이션 + +Database migrations run automatically before the application starts when using Docker. + +For manual migration in development: +```bash +npm run drizzle:migrate +``` + +Docker 사용 시 데이터베이스 마이그레이션은 애플리케이션 시작 전에 자동으로 실행됩니다. + +개발 환경에서 수동으로 마이그레이션 실행: +```bash +npm run drizzle:migrate +``` + diff --git a/scripts/migrate.js b/scripts/migrate.js new file mode 100755 index 0000000..fa033bd --- /dev/null +++ b/scripts/migrate.js @@ -0,0 +1,138 @@ +#!/usr/bin/env node +/** + * Standalone migration script + * Runs database migrations independently from application startup + */ + +import postgres from 'postgres'; +import { readdir, readFile } from 'fs/promises'; +import { join } from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Simple logger for migration script +const logger = { + info: (...args) => console.log('[INFO]', ...args), + error: (...args) => console.error('[ERROR]', ...args), + debug: (...args) => console.log('[DEBUG]', ...args) +}; + +// Read environment variables +const POSTGRESQL_USER = process.env.POSTGRESQL_USER || 'postgres'; +const POSTGRESQL_PASSWORD = process.env.POSTGRESQL_PASSWORD || 'postgres'; +const POSTGRESQL_HOST = process.env.POSTGRESQL_HOST || 'localhost'; +const POSTGRESQL_PORT = process.env.POSTGRESQL_PORT || '5432'; +const POSTGRESQL_NAME = process.env.POSTGRESQL_NAME || 'pjsedb'; +const POSTGRESQL_SSL = process.env.POSTGRESQL_SSL || 'disable'; + +const encodedPassword = encodeURIComponent(POSTGRESQL_PASSWORD); +const dbUrl = `postgres://${POSTGRESQL_USER}:${encodedPassword}@${POSTGRESQL_HOST}:${POSTGRESQL_PORT}/${POSTGRESQL_NAME}`; + +const msToSec = (v) => v ? Math.max(1, Math.ceil(v / 1000)) : undefined; + +// Create PostgreSQL client +const postgresClient = postgres(dbUrl, { + max: 10, + idle_timeout: msToSec(10000), + max_lifetime: msToSec(60000), + ssl: POSTGRESQL_SSL === 'disable' ? false : POSTGRESQL_SSL, +}); + +/** + * Run database migrations + */ +async function runMigrations() { + const migrationFolder = join(dirname(__dirname), 'drizzle'); + + try { + // Test database connection + await postgresClient`SELECT 1`; + logger.info('Connected to PostgreSQL'); + + // Create migrations meta table + await postgresClient` + CREATE TABLE IF NOT EXISTS drizzle_migrations ( + id SERIAL PRIMARY KEY, + hash TEXT NOT NULL, + created_at BIGINT + ) + `; + + // Check if migration directory exists + try { + await readdir(migrationFolder); + } catch (error) { + if (error.code === 'ENOENT') { + logger.info(`Migration folder '${migrationFolder}' does not exist. No migrations to run.`); + return; + } + throw error; + } + + // Read migration files + const files = await readdir(migrationFolder); + const sqlFiles = files + .filter(f => f.endsWith('.sql')) + .sort(); // Sort by filename + + if (sqlFiles.length === 0) { + logger.info('No migration files found'); + return; + } + + logger.info(`Found ${sqlFiles.length} migration file(s)`); + + for (const file of sqlFiles) { + const filePath = join(migrationFolder, file); + const sql = await readFile(filePath, 'utf-8'); + + // Execute in transaction with migration existence check inside for atomicity + await postgresClient.begin(async (tx) => { + // Check if migration already applied (inside transaction for atomicity) + const [existing] = await tx` + SELECT id FROM drizzle_migrations WHERE hash = ${file} + `; + + if (!existing) { + logger.info(`Running migration: ${file}`); + + // Execute SQL + await tx.unsafe(sql); + + // Record execution + await tx.unsafe( + 'INSERT INTO drizzle_migrations (hash, created_at) VALUES ($1, $2)', + [file, Date.now()] + ); + + logger.info(`Migration completed: ${file}`); + } else { + logger.debug(`Migration already applied: ${file}`); + } + }); + } + + logger.info('All migrations completed successfully'); + } catch (error) { + logger.error('Migration error:', error); + throw error; + } finally { + // Close connection + await postgresClient.end({ timeout: 5 }); + logger.info('PostgreSQL connection closed'); + } +} + +// Run migrations and exit +runMigrations() + .then(() => { + logger.info('Migration script completed successfully'); + process.exit(0); + }) + .catch((error) => { + logger.error('Migration script failed:', error); + process.exit(1); + }); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 548de62..c48413b 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,6 +1,6 @@ import type { Handle, ServerInit } from '@sveltejs/kit'; import { paraglideMiddleware } from '$lib/paraglide/server'; -import { initPostgres, runMigrations } from "$lib/server/postgresql/db"; +import { initPostgres } from "$lib/server/postgresql/db"; import { initRedis } from "$lib/server/redis/db"; import { sequence } from "@sveltejs/kit/hooks"; import { Cron } from "croner"; @@ -26,7 +26,6 @@ export const init: ServerInit = async () => { await initPostgres(); await initRedis(); - await runMigrations(); if (!newLoad) { newLoad = true;