mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
feat(docker): add Docker support with configuration files and entrypoint scripts
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import 'dotenv/config';
|
||||
import { Client } from 'pg';
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
type MigrationRow = {
|
||||
migration_name: string;
|
||||
finished_at: Date | null;
|
||||
rolled_back_at: Date | null;
|
||||
};
|
||||
|
||||
async function runPrisma(args: string[]) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn('./node_modules/.bin/prisma', args, {
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
proc.on('error', reject);
|
||||
proc.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error(`Prisma command failed: prisma ${args.join(' ')}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function tableExists(client: Client, tableName: string) {
|
||||
const result = await client.query<{ exists: boolean }>(
|
||||
`
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = $1
|
||||
) AS "exists"
|
||||
`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
return result.rows[0]?.exists ?? false;
|
||||
}
|
||||
|
||||
async function getMigrationRows(client: Client) {
|
||||
const hasMigrationsTable = await tableExists(client, '_prisma_migrations');
|
||||
if (!hasMigrationsTable) return [];
|
||||
|
||||
const result = await client.query<MigrationRow>(
|
||||
`
|
||||
SELECT migration_name, finished_at, rolled_back_at
|
||||
FROM "_prisma_migrations"
|
||||
ORDER BY started_at ASC
|
||||
`
|
||||
);
|
||||
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
function getMigrationDirectories() {
|
||||
return readdirSync(join(process.cwd(), 'prisma', 'migrations'), { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.sort();
|
||||
}
|
||||
|
||||
async function getPublicTables(client: Client) {
|
||||
const result = await client.query<{ table_name: string }>(
|
||||
`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY table_name ASC
|
||||
`
|
||||
);
|
||||
|
||||
return result.rows.map((row) => row.table_name);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error('DATABASE_URL is required');
|
||||
}
|
||||
|
||||
const client = new Client({ connectionString: process.env.DATABASE_URL });
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
const [publicTables, migrationRows] = await Promise.all([
|
||||
getPublicTables(client),
|
||||
getMigrationRows(client),
|
||||
]);
|
||||
|
||||
const appTables = publicTables.filter((table) => table !== '_prisma_migrations');
|
||||
const hasUsersTable = appTables.includes('users');
|
||||
const hasWorkspacesTable = appTables.includes('workspaces');
|
||||
const hasProjectsTable = appTables.includes('projects');
|
||||
const hasCoreTables = hasUsersTable || hasWorkspacesTable || hasProjectsTable;
|
||||
const failedMigrations = migrationRows.filter((row) => !row.finished_at && !row.rolled_back_at);
|
||||
const shouldBootstrapFreshSchema = !hasCoreTables;
|
||||
|
||||
console.log(`Detected public tables: ${appTables.length > 0 ? appTables.join(', ') : '(none)'}`);
|
||||
|
||||
if (shouldBootstrapFreshSchema) {
|
||||
if (failedMigrations.length > 0) {
|
||||
console.log('Detected failed migration state on a fresh database. Marking failed migrations as rolled back.');
|
||||
for (const migration of failedMigrations) {
|
||||
await runPrisma(['migrate', 'resolve', '--rolled-back', migration.migration_name]);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Fresh self-hosted database detected. Synchronizing schema baseline.');
|
||||
await runPrisma(['db', 'push']);
|
||||
|
||||
const appliedMigrationNames = new Set(
|
||||
migrationRows
|
||||
.filter((row) => row.finished_at && !row.rolled_back_at)
|
||||
.map((row) => row.migration_name)
|
||||
);
|
||||
|
||||
for (const migrationName of getMigrationDirectories()) {
|
||||
if (appliedMigrationNames.has(migrationName)) continue;
|
||||
await runPrisma(['migrate', 'resolve', '--applied', migrationName]);
|
||||
}
|
||||
|
||||
console.log('Fresh database bootstrap complete');
|
||||
return;
|
||||
}
|
||||
|
||||
if (failedMigrations.length > 0) {
|
||||
throw new Error(
|
||||
`Detected failed Prisma migrations on a non-empty database: ${failedMigrations
|
||||
.map((migration) => migration.migration_name)
|
||||
.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Running Prisma migrations');
|
||||
await runPrisma(['migrate', 'deploy']);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Docker database bootstrap failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
DB_HOST="${DOCKER_DB_HOST:-postgres}"
|
||||
DB_PORT="${DOCKER_DB_PORT:-5432}"
|
||||
MINIO_HEALTHCHECK_URL="${MINIO_HEALTHCHECK_URL:-http://minio:9000/minio/health/live}"
|
||||
MAX_ATTEMPTS="${STARTUP_MAX_ATTEMPTS:-60}"
|
||||
SLEEP_SECONDS="${STARTUP_SLEEP_SECONDS:-2}"
|
||||
|
||||
wait_for_tcp() {
|
||||
host="$1"
|
||||
port="$2"
|
||||
label="$3"
|
||||
attempt=1
|
||||
|
||||
while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do
|
||||
if nc -z "$host" "$port" >/dev/null 2>&1; then
|
||||
echo "$label is reachable at $host:$port"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Waiting for $label at $host:$port ($attempt/$MAX_ATTEMPTS)"
|
||||
attempt=$((attempt + 1))
|
||||
sleep "$SLEEP_SECONDS"
|
||||
done
|
||||
|
||||
echo "Timed out waiting for $label at $host:$port" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
wait_for_http() {
|
||||
url="$1"
|
||||
label="$2"
|
||||
attempt=1
|
||||
|
||||
while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do
|
||||
if curl --silent --fail "$url" >/dev/null 2>&1; then
|
||||
echo "$label is reachable at $url"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Waiting for $label at $url ($attempt/$MAX_ATTEMPTS)"
|
||||
attempt=$((attempt + 1))
|
||||
sleep "$SLEEP_SECONDS"
|
||||
done
|
||||
|
||||
echo "Timed out waiting for $label at $url" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
wait_for_tcp "$DB_HOST" "$DB_PORT" "Postgres"
|
||||
wait_for_http "$MINIO_HEALTHCHECK_URL" "MinIO"
|
||||
|
||||
echo "Bootstrapping database"
|
||||
bun run scripts/docker-db-bootstrap.ts
|
||||
|
||||
echo "Running self-host bootstrap"
|
||||
bun run self-host:bootstrap
|
||||
|
||||
echo "Starting OpenFrame"
|
||||
exec bun run start
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'dotenv/config';
|
||||
import { ensureR2BucketExists, R2_BUCKET_NAME } from '@/lib/r2';
|
||||
|
||||
const shouldCreateBucket = /^(1|true|yes|on)$/i.test(process.env.SELF_HOSTED_AUTO_CREATE_BUCKET ?? '');
|
||||
|
||||
async function main() {
|
||||
if (!shouldCreateBucket) {
|
||||
console.log('Skipping self-host bucket bootstrap');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Ensuring object storage bucket exists: ${R2_BUCKET_NAME}`);
|
||||
await ensureR2BucketExists();
|
||||
console.log(`Bucket is ready: ${R2_BUCKET_NAME}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Self-host bootstrap failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user