Decouple database migrations from server startup (#265)
* Use Docker Compose init service pattern for database migrations Replaces the fragile double-migration approach (server.ts startup + drizzle-kit CLI in CI) with a one-shot migrate service in Docker Compose that the app depends on via condition: service_completed_successfully. - Add scripts/migrate.mjs: programmatic drizzle-orm migration (uses production deps, not drizzle-kit which is devOnly and absent from image) - Dockerfile: copy scripts/ into image, remove drizzle.config.ts (only needed by drizzle-kit CLI) - server.ts: remove runMigrations() entirely; migrations are now handled by the migrate container before the app starts - deploy.yml: remove explicit docker run migration step; replace sleep 10 health check with a poll loop and explicit migrate exit-code check Production server's docker-compose.yaml needs a one-time manual update to add the migrate service — see plan for exact config. https://claude.ai/code/session_01ReaqH3o9NVH4QU4qE9WMMQ * Move drizzle-kit to devDependencies; use docker compose wait in deploy drizzle-kit is a dev-only tool (schema generation and local migrations). Now that production migrations run via the programmatic drizzle-orm API in the migrate init container, drizzle-kit has no runtime role. Also replaces the polling health check loop with docker compose wait, which blocks until the migrate service exits and returns its exit code cleanly — no sleep or manual status inspection needed. https://claude.ai/code/session_01ReaqH3o9NVH4QU4qE9WMMQ --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
89d52cca5e
commit
f1af4b5171
5 changed files with 38 additions and 34 deletions
15
.github/workflows/deploy.yml
vendored
15
.github/workflows/deploy.yml
vendored
|
|
@ -138,24 +138,21 @@ jobs:
|
||||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
CONTAINER_REGISTRY: ${{ vars.CONTAINER_REGISTRY }}
|
CONTAINER_REGISTRY: ${{ vars.CONTAINER_REGISTRY }}
|
||||||
PROD_DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
|
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.DEPLOY_HOST }}
|
host: ${{ secrets.DEPLOY_HOST }}
|
||||||
port: ${{ secrets.DEPLOY_PORT }}
|
port: ${{ secrets.DEPLOY_PORT }}
|
||||||
username: ${{ secrets.DEPLOY_USER }}
|
username: ${{ secrets.DEPLOY_USER }}
|
||||||
key: ${{ secrets.DEPLOY_KEY }}
|
key: ${{ secrets.DEPLOY_KEY }}
|
||||||
envs: REGISTRY_USERNAME,REGISTRY_PASSWORD,CONTAINER_REGISTRY,PROD_DATABASE_URL
|
envs: REGISTRY_USERNAME,REGISTRY_PASSWORD,CONTAINER_REGISTRY
|
||||||
script: |
|
script: |
|
||||||
set -e
|
set -e
|
||||||
IMAGE="$CONTAINER_REGISTRY/brackt:latest"
|
|
||||||
docker login $CONTAINER_REGISTRY -u $REGISTRY_USERNAME -p $REGISTRY_PASSWORD
|
docker login $CONTAINER_REGISTRY -u $REGISTRY_USERNAME -p $REGISTRY_PASSWORD
|
||||||
cd brackt
|
cd brackt
|
||||||
docker compose pull
|
docker compose pull
|
||||||
docker run --rm -e DATABASE_URL="$PROD_DATABASE_URL" "$IMAGE" npx drizzle-kit migrate
|
docker compose up -d migrate
|
||||||
docker compose up -d
|
if ! docker compose wait migrate | grep -qx "0"; then
|
||||||
sleep 10
|
echo "Migration failed:"
|
||||||
if docker compose ps | grep -qE "Exit|exited"; then
|
docker compose logs migrate
|
||||||
echo "One or more containers failed to start:"
|
|
||||||
docker compose logs --tail=50
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
docker compose up -d --remove-orphans
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
COPY --from=build-env /app/build /app/build
|
COPY --from=build-env /app/build /app/build
|
||||||
COPY --from=build-env /app/dist /app/dist
|
COPY --from=build-env /app/dist /app/dist
|
||||||
COPY ./drizzle /app/drizzle
|
COPY ./drizzle /app/drizzle
|
||||||
COPY ./drizzle.config.ts /app/drizzle.config.ts
|
COPY ./scripts /app/scripts
|
||||||
COPY ./instrument.server.mjs /app/instrument.server.mjs
|
COPY ./instrument.server.mjs /app/instrument.server.mjs
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
CMD ["npm", "run", "start"]
|
CMD ["npm", "run", "start"]
|
||||||
|
|
@ -56,7 +56,6 @@
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"compression": "^1.8.0",
|
"compression": "^1.8.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-kit": "~0.28.1",
|
|
||||||
"drizzle-orm": "~0.36.3",
|
"drizzle-orm": "~0.36.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"isbot": "^5.1.27",
|
"isbot": "^5.1.27",
|
||||||
|
|
@ -103,6 +102,7 @@
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"cypress": "^14.5.4",
|
"cypress": "^14.5.4",
|
||||||
"dotenv-cli": "^8.0.0",
|
"dotenv-cli": "^8.0.0",
|
||||||
|
"drizzle-kit": "~0.28.1",
|
||||||
"esbuild": "^0.25.11",
|
"esbuild": "^0.25.11",
|
||||||
"jsdom": "^27.0.1",
|
"jsdom": "^27.0.1",
|
||||||
"oxlint": "^1.56.0",
|
"oxlint": "^1.56.0",
|
||||||
|
|
|
||||||
26
scripts/migrate.mjs
Normal file
26
scripts/migrate.mjs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||||
|
import postgres from "postgres";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
if (!process.env.DATABASE_URL) {
|
||||||
|
console.error("ERROR: DATABASE_URL is required");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Running database migrations...");
|
||||||
|
const client = postgres(process.env.DATABASE_URL, { max: 1 });
|
||||||
|
try {
|
||||||
|
const db = drizzle(client);
|
||||||
|
await migrate(db, { migrationsFolder: path.resolve(__dirname, "../drizzle") });
|
||||||
|
console.log("Migrations completed successfully");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Migration failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
23
server.ts
23
server.ts
|
|
@ -6,9 +6,6 @@ import { createServer } from "http";
|
||||||
import type { ViteDevServer } from "vite";
|
import type { ViteDevServer } from "vite";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { drizzle } from "drizzle-orm/postgres-js";
|
|
||||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
||||||
import postgres from "postgres";
|
|
||||||
import { logger } from "./server/logger";
|
import { logger } from "./server/logger";
|
||||||
|
|
||||||
// ESM module resolution helpers
|
// ESM module resolution helpers
|
||||||
|
|
@ -24,20 +21,6 @@ const CLIENT_ASSETS_PATH = path.join(PROJECT_ROOT, "build/client/assets");
|
||||||
const DEVELOPMENT = process.env.NODE_ENV === "development";
|
const DEVELOPMENT = process.env.NODE_ENV === "development";
|
||||||
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||||||
|
|
||||||
async function runMigrations(): Promise<void> {
|
|
||||||
if (!process.env.DATABASE_URL) {
|
|
||||||
throw new Error("DATABASE_URL is required");
|
|
||||||
}
|
|
||||||
const client = postgres(process.env.DATABASE_URL);
|
|
||||||
try {
|
|
||||||
const db = drizzle(client);
|
|
||||||
await migrate(db, { migrationsFolder: path.join(PROJECT_ROOT, "drizzle") });
|
|
||||||
logger.log("Migrations complete");
|
|
||||||
} finally {
|
|
||||||
await client.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createAppServer(): Promise<void> {
|
async function createAppServer(): Promise<void> {
|
||||||
const app: Express = express();
|
const app: Express = express();
|
||||||
|
|
||||||
|
|
@ -100,9 +83,7 @@ async function createAppServer(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the server with error handling
|
// Start the server with error handling
|
||||||
(DEVELOPMENT ? Promise.resolve() : runMigrations())
|
createAppServer().catch((error) => {
|
||||||
.then(createAppServer)
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error("Failed to start server:", error);
|
logger.error("Failed to start server:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue