diff --git a/.env.example b/.env.example index 16a71dc9f6..c7e4911e4a 100644 --- a/.env.example +++ b/.env.example @@ -37,6 +37,8 @@ INITIAL_PASSWORD=CHANGEME # Base directory for all persistent data (SQLite DB, logs, backups). # Used by: src/lib/db/core.ts — resolves the SQLite database file path. # Default: ~/.omniroute/ | Override for Docker or custom installations. +# Hint: When running in Docker, consider mounting a host directory here for data persistence across container restarts +# also if you want to share the same database as "npm run dev" use "./data" # DATA_DIR=/var/lib/omniroute # Encryption key for SQLite database encryption at rest. diff --git a/Dockerfile b/Dockerfile index 853d9dc4e9..56a95c5e5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,22 @@ -FROM node:24-trixie-slim AS builder +# ── Common base with runtime deps ────────────────────────────────────────── +FROM node:26-trixie-slim AS base WORKDIR /app -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=shared \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=shared \ apt-get update \ - && apt-get install -y --no-install-recommends libsecret-1-0 ca-certificates python3 make g++ \ + && apt-get install -y --no-install-recommends libsecret-1-0 ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# ── Builder ──────────────────────────────────────────────────────────────── +FROM base AS builder + +# Build tools for native module compilation +# apt-get update needed here because base's rm -rf clears the shared cache +RUN --mount=type=cache,target=/var/cache/apt,sharing=shared \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=shared \ + apt-get update \ + && apt-get install -y --no-install-recommends python3 make g++ \ && rm -rf /var/lib/apt/lists/* COPY package*.json ./ @@ -26,12 +38,15 @@ RUN --mount=type=cache,target=/root/.npm \ && npm rebuild better-sqlite3 \ && node -e "require('better-sqlite3')(':memory:').close()" +# Use Turbopack for significant build speedup +ENV OMNIROUTE_USE_TURBOPACK=1 + COPY . ./ RUN --mount=type=cache,target=/app/.next/cache \ - mkdir -p /app/data && npm run build -- --webpack + mkdir -p /app/data && npm run build -FROM node:24-trixie-slim AS runner-base -WORKDIR /app +# ── Runner base ──────────────────────────────────────────────────────────── +FROM base AS runner-base LABEL org.opencontainers.image.title="omniroute" \ org.opencontainers.image.description="Unified AI proxy — route any LLM through one endpoint" \ @@ -46,15 +61,11 @@ ENV NODE_OPTIONS="--max-old-space-size=256" # Data directory inside Docker — must match the volume mount in docker-compose.yml ENV DATA_DIR=/app/data -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ - apt-get update \ - && apt-get install -y --no-install-recommends libsecret-1-0 ca-certificates \ - && rm -rf /var/lib/apt/lists/* RUN mkdir -p /app/data -COPY --from=builder /app/public ./public -COPY --from=builder /app/.next/static ./.next/static +# The standalone build + syncStandaloneExtraModules bundles all runtime files +# (.next, node_modules, migrations, scripts, docs, etc.) into .next/standalone/. +# Explicit overrides below cover modules that NFT tracing may miss. COPY --from=builder /app/.next/standalone ./ # Explicitly copy @swc/helpers — not always traced by standalone output but needed at runtime COPY --from=builder /app/node_modules/@swc/helpers ./node_modules/@swc/helpers @@ -70,18 +81,6 @@ COPY --from=builder /app/node_modules/split2 ./node_modules/split2 # traced by Next.js standalone output — copy them explicitly. COPY --from=builder /app/src/lib/db/migrations ./migrations ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations -# MITM server.cjs is spawned at runtime via child_process — not traced by nft -COPY --from=builder /app/src/mitm/server.cjs ./src/mitm/server.cjs -# Runtime docs are pruned by .dockerignore to English markdown + OpenAPI. -# Next.js standalone tracing does not include docs read via fs. -COPY --from=builder /app/.next/standalone/docs ./docs - -COPY --from=builder /app/scripts/dev/run-standalone.mjs ./dev/run-standalone.mjs -COPY --from=builder /app/scripts/build/runtime-env.mjs ./build/runtime-env.mjs -COPY --from=builder /app/scripts/build/bootstrap-env.mjs ./build/bootstrap-env.mjs -COPY --from=builder /app/scripts/dev/healthcheck.mjs ./healthcheck.mjs - -RUN node -e "require('better-sqlite3')(':memory:').close()" # Hand /app over to the baked-in `node` non-root user (UID/GID 1000) so the # runtime process never holds root privileges. The chown happens after all diff --git a/scripts/build/build-next-isolated.mjs b/scripts/build/build-next-isolated.mjs index 97e6792d7e..c251de747e 100644 --- a/scripts/build/build-next-isolated.mjs +++ b/scripts/build/build-next-isolated.mjs @@ -147,6 +147,18 @@ export async function syncStandaloneNativeAssets( sourcePath: path.join(rootDir, "node_modules", "wreq-js", "rust"), destinationPath: path.join(rootDir, ".next", "standalone", "node_modules", "wreq-js", "rust"), }, + { + label: "better-sqlite3 native binary", + sourcePath: path.join(rootDir, "node_modules", "better-sqlite3", "build"), + destinationPath: path.join( + rootDir, + ".next", + "standalone", + "node_modules", + "better-sqlite3", + "build" + ), + }, ]; let changed = false; @@ -171,6 +183,90 @@ export async function syncStandaloneNativeAssets( return changed; } +export async function syncStandaloneExtraModules( + rootDir = projectRoot, + fsImpl = fs, + log = console +) { + const entries = [ + { + label: "@swc/helpers", + sourcePath: path.join(rootDir, "node_modules", "@swc", "helpers"), + destRelative: path.join("node_modules", "@swc", "helpers"), + }, + { + label: "pino-abstract-transport", + sourcePath: path.join(rootDir, "node_modules", "pino-abstract-transport"), + destRelative: path.join("node_modules", "pino-abstract-transport"), + }, + { + label: "pino-pretty", + sourcePath: path.join(rootDir, "node_modules", "pino-pretty"), + destRelative: path.join("node_modules", "pino-pretty"), + }, + { + label: "split2", + sourcePath: path.join(rootDir, "node_modules", "split2"), + destRelative: path.join("node_modules", "split2"), + }, + { + label: "migrations", + sourcePath: path.join(rootDir, "src", "lib", "db", "migrations"), + destRelative: "migrations", + }, + { + label: "MITM server", + sourcePath: path.join(rootDir, "src", "mitm", "server.cjs"), + destRelative: path.join("src", "mitm", "server.cjs"), + }, + { + label: "run-standalone script", + sourcePath: path.join(rootDir, "scripts", "dev", "run-standalone.mjs"), + destRelative: path.join("dev", "run-standalone.mjs"), + }, + { + label: "runtime-env script", + sourcePath: path.join(rootDir, "scripts", "build", "runtime-env.mjs"), + destRelative: path.join("build", "runtime-env.mjs"), + }, + { + label: "bootstrap-env script", + sourcePath: path.join(rootDir, "scripts", "build", "bootstrap-env.mjs"), + destRelative: path.join("build", "bootstrap-env.mjs"), + }, + { + label: "healthcheck script", + sourcePath: path.join(rootDir, "scripts", "dev", "healthcheck.mjs"), + destRelative: "healthcheck.mjs", + }, + { + label: "public directory", + sourcePath: path.join(rootDir, "public"), + destRelative: "public", + }, + { + label: "playwright-core (dynamic import by gemini-web executor)", + sourcePath: path.join(rootDir, "node_modules", "playwright-core"), + destRelative: path.join("node_modules", "playwright-core"), + }, + ]; + + let changed = false; + const standaloneRoot = path.join(rootDir, ".next", "standalone"); + + for (const entry of entries) { + if (!(await exists(entry.sourcePath))) continue; + + const destPath = path.join(standaloneRoot, entry.destRelative); + await fsImpl.mkdir(path.dirname(destPath), { recursive: true }); + await fsImpl.cp(entry.sourcePath, destPath, { recursive: true, force: true }); + log.log(`[build-next-isolated] Synced standalone module: ${entry.label}`); + changed = true; + } + + return changed; +} + export async function main() { const movedPaths = []; const transientBuildPaths = getTransientBuildPaths(); @@ -225,6 +321,15 @@ export async function main() { nativeAssetErr ); } + + try { + await syncStandaloneExtraModules(projectRoot); + } catch (extraModuleErr) { + console.warn( + "[build-next-isolated] Non-fatal error syncing extra modules:", + extraModuleErr + ); + } } process.exitCode = result.code; } catch (error) {