feat(docker): add Docker support with configuration files and entrypoint scripts

This commit is contained in:
Yusuf İpek
2026-04-09 15:54:03 +03:00
parent b1b1715578
commit e97d60cf36
13 changed files with 658 additions and 31 deletions
+152
View File
@@ -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);
});
+61
View File
@@ -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
+20
View File
@@ -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);
});