fix: resolve self-hosting issues on Ubuntu without Docker - #7917
Arav-Menon wants to merge 5 commits into
Conversation
Detect Postgres error code 42P01 (undefined_table) in ensureAnonymousUserExists() and throw an actionable error message instead of a generic internal server error. This helps developers quickly identify that database migrations need to be run.
- Add DISABLE_TELEMETRY env var to skip all OTel initialization - Reduce export timeout from 30s to 10s for faster failure - Wrap sdk.shutdown() with 5s timeout to prevent blocking exit - Change process.on to process.once for SIGTERM/SIGINT handlers to prevent duplicate execution during HMR reloads
Match the default config in instrumentation-node.ts to prevent long shutdown hangs when the telemetry endpoint is unreachable.
Document the new env var that disables all OpenTelemetry collection, preventing export timeouts when the telemetry endpoint is unreachable.
renameSync fails with 'EXDEV: cross-device link not permitted' when moving files from /tmp (tmpfs) to the project directory (ext4) on Ubuntu. Use cpSync with recursive and force options instead, which works across filesystem boundaries.
|
@Arav-Menon is attempting to deploy a commit to the Sim Team on Vercel. A member of the Team first needs to authorize it. |
|
| process.once('SIGTERM', shutdownOtel) | ||
| process.once('SIGINT', shutdownOtel) |
There was a problem hiding this comment.
process.once() removes a listener only after it fires; it does not deduplicate listeners added by repeated instrumentation registration during HMR. Each reload therefore adds another OTel shutdown closure here and another PostHog closure at lines 409–410. All accumulated handlers run on the next signal, so duplicate shutdowns still occur. Make initialization idempotent or explicitly replace the previous listeners.
| const timeoutPromise = new Promise<void>((_, reject) => | ||
| setTimeout(() => reject(new Error('OTel shutdown timed out')), 5000) | ||
| ) | ||
| await Promise.race([shutdownPromise, timeoutPromise]) |
There was a problem hiding this comment.
Timeout Delays Successful Shutdown
When sdk.shutdown() finishes first, this five-second timer is neither cleared nor unreferenced. The installed SIGTERM and SIGINT listeners disable Node's default signal exit, so the pending timer keeps the event loop alive for the rest of those five seconds even after shutdown succeeds. Store and clear the timer in a finally block, or otherwise prevent it from keeping the process alive.
| const innerCode = (cause as Record<string, unknown>).code | ||
| if (innerCode === '42P01') return true | ||
| } | ||
| } |
There was a problem hiding this comment.
Nested Errors Remain Undetected
This checks only the top-level error and its immediate cause, although the repository's PostgreSQL error handling supports deeper cause chains. If Drizzle wraps 42P01 beneath two causes, this helper misses it and self-hosters still get the generic failure instead of the migration guidance. Reuse getPostgresErrorCode from @sim/utils, which already walks nested causes safely.
| const timeoutPromise = new Promise<void>((_, reject) => | ||
| setTimeout(() => reject(new Error('OTel shutdown timed out')), 5000) | ||
| ) |
There was a problem hiding this comment.
This change constructs a delay with new Promise and setTimeout, violating the repository directive to use the shared sleep(ms) helper from @sim/utils/helpers instead of inline timeout promises. This repository requirement must be satisfied before merging.
Context Used: CLAUDE.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| rmSync(target, { recursive: true, force: true }) | ||
| renameSync(join(staging, 'package'), target) | ||
| cpSync(join(staging, 'package'), target, { recursive: true, force: true }) |
There was a problem hiding this comment.
Copy Can Leave Partial Install
The script now deletes the working target and copies directly into that live directory, so interruption, disk exhaustion, or a copy error can leave a partial installation. A later run checks only whether pty.node exists, so a torn copy containing that file can be treated as valid and reused. Copy into a temporary directory on the destination filesystem first, then atomically rename it into place.
Overview
Fixes multiple issues reported in #6158 that break self-hosting Sim on Ubuntu 24.04 without Docker. The three bugs addressed are: silent failure when database migrations haven't been run, dev server killed by OTel export timeouts, and desktop build failure due to cross-device rename.
Changes
1. Better error handling for missing database migrations
File:
apps/sim/lib/auth/anonymous.tsWhen
DISABLE_AUTH=trueand the database has no tables (migrations not run),ensureAnonymousUserExists()would fail with a Postgres42P01(undefined_table) error. This error was caught bywithRouteHandlerand returned as a generic"Internal server error"to the client, leaving users with no guidance.Fix: Added
isMissingTableError()helper that detects Postgres error code42P01at any nesting level (Drizzle wraps it inerror.cause). When detected, throws a clear error:"Database tables not found. Run database migrations before starting the app: bun run db:migrate". This message appears in server logs for developers to see, while the client still receives a generic error (no internal details exposed to end users).2. Prevent OTel timeout from killing dev server
Files:
apps/sim/instrumentation-node.ts,apps/sim/telemetry.config.ts,apps/sim/.env.exampleThe OpenTelemetry SDK tries to export telemetry data to
telemetry.simstudio.ai. When this endpoint is unreachable (firewall, network issues, self-hosted environments), the export hangs for 30 seconds, the event loop drains, and the process exits. Additionally, SIGTERM handlers usedprocess.on()instead ofprocess.once(), causing duplicate handler execution during Next.js HMR reloads (visible as 3x "PostHog client shut down successfully" log lines).Fixes:
DISABLE_TELEMETRY=1env var that skips all OTel initialization before any dynamic imports (zero overhead)exportTimeoutMillisfrom 30s to 10s for faster failure when endpoint is unreachablesdk.shutdown()inPromise.racewith a 5s timeout — hard ceiling so shutdown can never block process exitprocess.on()toprocess.once()to prevent duplicate execution3. Fix desktop EXDEV cross-device rename
File:
apps/desktop/scripts/ensure-pty-prebuilds.tsThe desktop build script downloads a prebuilt binary to
/tmp(tmpfs) and then callsrenameSync()to move it to the project directory under/home(ext4). On Linux,rename(2)fails withEXDEV: cross-device link not permittedwhen source and destination are on different filesystems.Fix: Replaced
renameSync()withcpSync(..., { recursive: true, force: true })which works across filesystem boundaries. The staging directory is still cleaned up in thefinallyblock.Related