Skip to content

fix: resolve self-hosting issues on Ubuntu without Docker - #7917

Open
Arav-Menon wants to merge 5 commits into
simstudioai:mainfrom
Arav-Menon:fix/self-hosting-bugs
Open

Arav-Menon wants to merge 5 commits into
simstudioai:mainfrom
Arav-Menon:fix/self-hosting-bugs

Conversation

@Arav-Menon

Copy link
Copy Markdown

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.ts

When DISABLE_AUTH=true and the database has no tables (migrations not run), ensureAnonymousUserExists() would fail with a Postgres 42P01 (undefined_table) error. This error was caught by withRouteHandler and returned as a generic "Internal server error" to the client, leaving users with no guidance.

Fix: Added isMissingTableError() helper that detects Postgres error code 42P01 at any nesting level (Drizzle wraps it in error.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.example

The 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 used process.on() instead of process.once(), causing duplicate handler execution during Next.js HMR reloads (visible as 3x "PostHog client shut down successfully" log lines).

Fixes:

  • Added DISABLE_TELEMETRY=1 env var that skips all OTel initialization before any dynamic imports (zero overhead)
  • Reduced exportTimeoutMillis from 30s to 10s for faster failure when endpoint is unreachable
  • Wrapped sdk.shutdown() in Promise.race with a 5s timeout — hard ceiling so shutdown can never block process exit
  • Changed all 4 SIGTERM/SIGINT handlers from process.on() to process.once() to prevent duplicate execution

3. Fix desktop EXDEV cross-device rename

File: apps/desktop/scripts/ensure-pty-prebuilds.ts

The desktop build script downloads a prebuilt binary to /tmp (tmpfs) and then calls renameSync() to move it to the project directory under /home (ext4). On Linux, rename(2) fails with EXDEV: cross-device link not permitted when source and destination are on different filesystems.

Fix: Replaced renameSync() with cpSync(..., { recursive: true, force: true }) which works across filesystem boundaries. The staging directory is still cleaned up in the finally block.

Related

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.
@vercel

vercel Bot commented Sep 17, 2026

Copy link
Copy Markdown

@Arav-Menon is attempting to deploy a commit to the Sim Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps

greptile-apps Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 2/5

The PR is not yet safe to merge because repeated instrumentation registration still duplicates shutdown handlers, successful shutdown can be delayed by its own timeout timer, and deeply wrapped missing-table errors remain undetected.

Findings

  1. P1 Handlers Still Accumulate
  2. P1 Timeout Delays Successful Shutdown
  3. P1 Nested Errors Remain Undetected
  4. P2 Shared Delay Helper Bypassed
  5. P2 Copy Can Leave Partial Install

Summary

This PR improves Ubuntu self-hosting diagnostics, adds an OpenTelemetry opt-out and shorter export timeout, adjusts shutdown signal handling, and replaces a cross-filesystem desktop prebuild rename with a recursive copy.

  • Adds actionable missing-migration guidance for anonymous authentication.
  • Adds DISABLE_TELEMETRY=1 and shorter OTel export timeouts.
  • Adds a bounded OTel shutdown race and changes signal listeners to one-shot handlers.
  • Makes desktop prebuild installation work across filesystems.
  • The new signal handling still accumulates handlers during repeated registration, the timeout leaves a live timer, and missing-table detection does not traverse all supported error wrappers.

Reviews (1) · Last reviewed commit: "fix: replace renameSync with cpSync to p..."

Comment on lines +377 to +378
process.once('SIGTERM', shutdownOtel)
process.once('SIGINT', shutdownOtel)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Handlers Still Accumulate

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.

Comment on lines +367 to +370
const timeoutPromise = new Promise<void>((_, reject) =>
setTimeout(() => reject(new Error('OTel shutdown timed out')), 5000)
)
await Promise.race([shutdownPromise, timeoutPromise])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +18 to +21
const innerCode = (cause as Record<string, unknown>).code
if (innerCode === '42P01') return true
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +367 to +369
const timeoutPromise = new Promise<void>((_, reject) =>
setTimeout(() => reject(new Error('OTel shutdown timed out')), 5000)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Shared Delay Helper Bypassed

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!

Comment on lines 105 to +106
rmSync(target, { recursive: true, force: true })
renameSync(join(staging, 'package'), target)
cpSync(join(staging, 'package'), target, { recursive: true, force: true })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Self-hosting on Ubuntu 24.04 without Docker is broken — SIGTERM kills dev server, build fails, no DB seed

1 participant