From e6dec1eb95bb924ea2ef2b4637f7e19dcd561b8e Mon Sep 17 00:00:00 2001 From: emil Date: Sat, 16 May 2026 00:29:32 +0300 Subject: [PATCH] fix(deploy): apply drizzle migrations to prod on container startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deploy pipeline was building the app image with only `dist/` inside and rsync'ing only `dist package*.json docker-compose.yml Dockerfile` to the server. The drizzle migrations folder never reached prod, so new schema changes (Wave 1 added `tier`, `boosty_verified_at`, and 5 tables) silently went missing. App requests then failed with "column does not exist" errors at runtime. Wave-1 CI step `npm run db:migrate` ran against the in-job postgres service, not against prod — so it never helped. Changes: - src/db/migrate.ts -> src/db/migrate.mjs: plain JS so the prod image can run it via `node` without devDependencies (no tsx needed). - Dockerfile: COPY drizzle and src/db/migrate.mjs into the image, prepend `node ./src/db/migrate.mjs &&` to the CMD. Container fails to start if migrations fail — better than serving with stale schema. - .github/workflows/deploy.yml: rsync now also sends `drizzle` and `src` so the build context on the server has what the Dockerfile COPYs reference. - package.json: `db:migrate` script switched to `node src/db/migrate.mjs`. - eslint.config.mjs: enable node globals for the migrator script. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/deploy.yml | 4 +++- Dockerfile | 6 +++++- eslint.config.mjs | 8 ++++++++ package.json | 2 +- src/db/{migrate.ts => migrate.mjs} | 21 ++++++++++++--------- 5 files changed, 29 insertions(+), 12 deletions(-) rename src/db/{migrate.ts => migrate.mjs} (57%) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cc0d105..aa535b0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -50,9 +50,11 @@ jobs: - name: Deploy Node.js app run: | + # drizzle/ and src/db/migrate.mjs are needed inside the Docker + # build context so the image can apply pending migrations on startup. rsync -avz --delete \ -e "ssh -i ~/.ssh/deploy_key" \ - dist package*.json docker-compose.yml Dockerfile \ + dist drizzle src package*.json docker-compose.yml Dockerfile \ ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:~/www/randify.pro/ - name: Restart app diff --git a/Dockerfile b/Dockerfile index 7d13e01..9038b0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,11 @@ WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY dist ./dist +COPY drizzle ./drizzle +COPY src/db/migrate.mjs ./src/db/migrate.mjs EXPOSE 4321 ENV HOST=0.0.0.0 ENV PORT=4321 -CMD ["node", "./dist/server/entry.mjs"] +# Run pending migrations on startup, then start the app. +# Fails the container if migrations fail (so we don't serve with a stale schema). +CMD ["sh", "-c", "node ./src/db/migrate.mjs && node ./dist/server/entry.mjs"] diff --git a/eslint.config.mjs b/eslint.config.mjs index 6d30944..a3fc1b9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -19,4 +19,12 @@ export default [ "@typescript-eslint/no-unused-vars": "off", }, }, + { + files: ["src/db/migrate.mjs"], + languageOptions: { + globals: { + ...globals.node, + }, + }, + }, ]; diff --git a/package.json b/package.json index 152e47f..8c26502 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "format": "prettier --write .", "format:check": "prettier --check .", "db:generate": "drizzle-kit generate", - "db:migrate": "tsx src/db/migrate.ts", + "db:migrate": "node src/db/migrate.mjs", "db:studio": "drizzle-kit studio" }, "dependencies": { diff --git a/src/db/migrate.ts b/src/db/migrate.mjs similarity index 57% rename from src/db/migrate.ts rename to src/db/migrate.mjs index e13c053..6f3e472 100644 --- a/src/db/migrate.ts +++ b/src/db/migrate.mjs @@ -1,21 +1,24 @@ import { drizzle } from 'drizzle-orm/node-postgres'; import { migrate } from 'drizzle-orm/node-postgres/migrator'; -import { Pool } from 'pg'; +import pg from 'pg'; -const pool = new Pool({ - connectionString: process.env.DATABASE_URL, -}); +const connectionString = process.env.DATABASE_URL; +if (!connectionString) { + console.error('Migration aborted: DATABASE_URL is not set'); + process.exit(1); +} +const pool = new pg.Pool({ connectionString }); const db = drizzle(pool); -async function runMigrations() { +try { console.log('Running migrations...'); await migrate(db, { migrationsFolder: './drizzle' }); console.log('Migrations completed.'); +} catch (err) { + console.error('Migration failed:', err); await pool.end(); + process.exit(1); } -runMigrations().catch((err) => { - console.error('Migration failed:', err); - process.exit(1); -}); +await pool.end();