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
This commit is contained in:
Claude 2026-04-06 03:57:43 +00:00
parent 89d52cca5e
commit 8e38ea8569
No known key found for this signature in database
4 changed files with 54 additions and 33 deletions

View file

@ -138,24 +138,38 @@ 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 --remove-orphans
docker compose up -d
sleep 10 echo "Waiting for app to start..."
if docker compose ps | grep -qE "Exit|exited"; then for i in $(seq 1 12); do
echo "One or more containers failed to start:" STATUS=$(docker inspect --format='{{.State.Status}}' brackt 2>/dev/null || echo "missing")
docker compose logs --tail=50 if [ "$STATUS" = "running" ]; then
echo "App is running"
break
fi
if [ $i -eq 12 ]; then
echo "App failed to start. Logs:"
docker logs brackt-migrate --tail=30 2>/dev/null || true
docker logs brackt --tail=30 2>/dev/null || true
exit 1
fi
sleep 5
done
MIGRATE_EXIT=$(docker inspect --format='{{.State.ExitCode}}' brackt-migrate 2>/dev/null || echo "1")
if [ "$MIGRATE_EXIT" != "0" ]; then
echo "Migration failed:"
docker logs brackt-migrate
exit 1 exit 1
fi fi

View file

@ -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"]

26
scripts/migrate.mjs Normal file
View 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();
}

View file

@ -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) logger.error("Failed to start server:", error);
.catch((error) => { process.exit(1);
logger.error("Failed to start server:", error); });
process.exit(1);
});