diff --git a/Dockerfile.selfhost b/Dockerfile.selfhost index 383650e..e10ad08 100644 --- a/Dockerfile.selfhost +++ b/Dockerfile.selfhost @@ -16,17 +16,13 @@ COPY . . EXPOSE 3001 # Readiness probe against the unauthenticated setup/health endpoint. The long -# start period covers migrations plus the boot-time vite build below. +# start period covers migrations plus the boot-time vite build (skipped when +# the previous start's build is still valid; see docker-entrypoint.sh). HEALTHCHECK --interval=30s --timeout=10s --start-period=300s --retries=3 \ CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3001)+'/api/health').then(function(r){process.exit(r.ok?0:1)}).catch(function(){process.exit(1)})" -# The preflight validates env BEFORE the slow steps, so misconfiguration fails -# in seconds with the exact fix instead of after a multi-minute build. -# -# The build MUST run at container start, not image-build time: AUTH_MODE (and the -# other client envs) are inlined into the client bundle by `vite build`, and the -# self-hoster only chooses AUTH_MODE at runtime via Compose. Building here lets -# that runtime value bake into the bundle. The repo .npmrc raises the V8 heap -# ceiling (node-options) so the ~7400-module SSR build doesn't OOM under Node's -# ~2GB default. -CMD ["sh", "-c", "echo 'OpenSEO sends an anonymous usage heartbeat (counts only). Disable: OPENSEO_TELEMETRY_DISABLED=1. Details: docs/SELF_HOSTING_DOCKER.md#telemetry' && pnpm exec tsx scripts/selfhost-preflight.ts && pnpm run db:migrate:local && pnpm run build && pnpm exec vite preview --host 0.0.0.0 --port ${PORT:-3001}"] +# Startup (preflight, migrations, conditional build, serve) lives in +# docker-entrypoint.sh. The repo .npmrc raises the V8 heap ceiling +# (node-options) so the ~7400-module SSR build doesn't OOM under Node's ~2GB +# default. +CMD ["sh", "docker-entrypoint.sh"] diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..c311cd9 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# Self-host container entrypoint. vite build inlines the envPrefix'd client +# envs (see vite.config.ts) into the bundle, so the build must run at container +# start — but the output stays valid until those envs or the image change. +# Fingerprint them and skip the build when the last start's output matches; an +# image update lands a fresh container with no build output, so new code always +# rebuilds. +set -e + +echo 'OpenSEO sends an anonymous usage heartbeat (counts only). Disable: OPENSEO_TELEMETRY_DISABLED=1. Details: docs/SELF_HOSTING_DOCKER.md#telemetry' + +# The preflight validates env BEFORE the slow steps, so misconfiguration fails +# in seconds with the exact fix instead of after a multi-minute build. +pnpm exec tsx scripts/selfhost-preflight.ts + +pnpm run db:migrate:local + +# POSTHOG_SOURCEMAPS (CI sourcemap uploads) moves vite's outDir; keep the +# fingerprint marker beside the output it describes. +if [ "${POSTHOG_SOURCEMAPS:-}" = "true" ]; then OUT_DIR=dist-sourcemaps; else OUT_DIR=dist; fi +FP_FILE="$OUT_DIR/.openseo-build-env" + +# Everything that changes build output: the envPrefix prefixes from +# vite.config.ts (keep in sync) plus POSTHOG_SOURCEMAPS. +FINGERPRINT="$(env | grep -E '^(VITE_|AUTH_MODE|BYPASS_EMAIL_VERIFICATION|POSTHOG_PUBLIC_KEY|POSTHOG_HOST|TURNSTILE_SITE_KEY|POSTHOG_SOURCEMAPS)' | sort | sha256sum | cut -d' ' -f1)" +# A missing sha256sum would yield an empty, always-matching fingerprint and +# silently disable rebuilds — fail loudly instead. +test -n "$FINGERPRINT" + +if [ -f "$FP_FILE" ] && [ "$(cat "$FP_FILE")" = "$FINGERPRINT" ]; then + echo "Reusing existing build (build-relevant env unchanged)." +else + echo "Building client + server (first start, changed build env, or new image)..." + rm -f "$FP_FILE" + pnpm run build + printf '%s' "$FINGERPRINT" > "$FP_FILE" +fi + +exec pnpm exec vite preview --host 0.0.0.0 --port "${PORT:-3001}" diff --git a/docs/SELF_HOSTING_DOCKER.md b/docs/SELF_HOSTING_DOCKER.md index 0c142a1..bcea3d5 100644 --- a/docs/SELF_HOSTING_DOCKER.md +++ b/docs/SELF_HOSTING_DOCKER.md @@ -25,7 +25,7 @@ Set `DATAFORSEO_API_KEY` in `.env` using the [DataForSEO setup guide](./DATAFORS docker compose up -d ``` -Open `http://localhost:` (default `3001`). Each container start builds the app and may take 1-2 minutes; follow progress with `docker compose logs -f`. +Open `http://localhost:` (default `3001`). The first start builds the app and may take 1-2 minutes; follow progress with `docker compose logs -f`. Optional env values: diff --git a/src/lib/selfhost-preflight.ts b/src/lib/selfhost-preflight.ts index d85db81..eb0555e 100644 --- a/src/lib/selfhost-preflight.ts +++ b/src/lib/selfhost-preflight.ts @@ -270,7 +270,7 @@ export function formatPreflightReport(result: PreflightResult): string { lines.push( result.failed ? "\nPreflight failed — fix the [FAIL] items above and restart. Nothing was started." - : "\nPreflight passed. The app now builds inside the container (~1-2 minutes on every start before it serves).", + : "\nPreflight passed. The app now builds inside the container (~1-2 minutes on first start before it serves).", ); return lines.join("\n");