* Optimize self-host Docker image size Reduce the self-host runtime image by splitting production deps into a parallel stage, using a distroless cc runtime with a copied Bun binary, moving web-only host dependencies to devDependencies, and copying/pruning only runtime-needed files. Verification: DOCKER_BUILDKIT=1 docker build --no-cache --progress=plain -f apps/host-selfhost/Dockerfile -t executor-selfhost-pr:latest . docker run smoke: /api/health returned OK Result: image_mb=239.681 * Baseline self-host Docker image size after current Dockerfile optimization Result: {"status":"keep","image_mb":239.68} * Simplify self-host Docker runtime packaging * Bundle self-host server for Docker runtime * Remove autoresearch log from Docker image PR * Move self-host runtime packaging into app * Format self-host package manifest * Keep self-host app UI packages as dependencies
Unified · Split
| @@ -1,36 +1,27 @@ | |||
| 1 | # Self-hosted Executor — single container, no external services. | 1 | # Self-hosted Executor, single container, no external services. |
| 2 | # Build context is the REPO ROOT (the bun workspace install needs every member): | 2 | # Build context is the repo root because Bun workspaces need every member: |
| 3 | # docker build -f apps/host-selfhost/Dockerfile -t executor-selfhost . | 3 | # docker build -f apps/host-selfhost/Dockerfile -t executor-selfhost . |
| 4 | # | 4 | # |
| 5 | # Runtime needs nothing but this container + a volume for the data dir: | 5 | # Runtime needs this container plus a volume for the data dir: |
| 6 | # docker run -p 4788:4788 -e BETTER_AUTH_SECRET=$(openssl rand -hex 32) \ | 6 | # docker run -p 4788:4788 -e BETTER_AUTH_SECRET=$(openssl rand -hex 32) \ |
| 7 | # -e EXECUTOR_BOOTSTRAP_ADMIN_EMAIL=you@example.com \ | 7 | # -e EXECUTOR_BOOTSTRAP_ADMIN_EMAIL=you@example.com \ |
| 8 | # -e EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD=... \ | 8 | # -e EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD=... \ |
| 9 | # -e EXECUTOR_WEB_BASE_URL=https://your.domain \ | 9 | # -e EXECUTOR_WEB_BASE_URL=https://your.domain \ |
| 10 | # -v executor-data:/data executor-selfhost | 10 | # -v executor-data:/data executor-selfhost |
| 11 | # | ||
| 12 | # SQLite (libSQL, file:/data/...) lives in /data, QuickJS + MCP run in-process — | ||
| 13 | # so there's no postgres/worker/proxy to orchestrate. | ||
| 14 | 11 | ||
| 15 | # ── Build stage: install the workspace + build the SPA ────────────────────── | 12 | FROM oven/bun:1 AS prod-deps |
| 13 | WORKDIR /app | ||
| 14 | COPY . . | ||
| 15 | RUN bun install --frozen-lockfile --production --ignore-scripts --filter @executor-js/host-selfhost \ | ||
| 16 | && bun run apps/host-selfhost/scripts/package-runtime.ts | ||
| 17 | |||
| 16 | FROM oven/bun:1 AS build | 18 | FROM oven/bun:1 AS build |
| 17 | WORKDIR /app | 19 | WORKDIR /app |
| 18 | COPY . . | 20 | COPY . . |
| 19 | # Full install (dev deps included — vite/turbo/plugins are needed to build). | ||
| 20 | RUN bun install --frozen-lockfile | 21 | RUN bun install --frozen-lockfile |
| 21 | # Builds @executor-js/vite-plugin (via turbo) then the self-host SPA into | ||
| 22 | # apps/host-selfhost/dist. | ||
| 23 | RUN cd apps/host-selfhost && bun run build | 22 | RUN cd apps/host-selfhost && bun run build |
| 24 | # Reinstall PRODUCTION deps only, so the runtime image excludes the build/dev | ||
| 25 | # toolchain pulled by the full workspace install — vite, turbo, wrangler → | ||
| 26 | # miniflare → sharp/libvips (~800MB), astro, vitest, etc. The built SPA lives in | ||
| 27 | # apps/host-selfhost/dist (outside node_modules), so it survives the reinstall. | ||
| 28 | # --ignore-scripts: the root `prepare` hook runs a dev-only tool | ||
| 29 | # (effect-language-service); runtime deps are prebuilt JS with no postinstall. | ||
| 30 | RUN rm -rf node_modules && bun install --frozen-lockfile --production --ignore-scripts | ||
| 31 | 23 | ||
| 32 | # ── Runtime stage: serve the built app under Bun ──────────────────────────── | 24 | FROM gcr.io/distroless/cc-debian12 AS runtime |
| 33 | FROM oven/bun:1 AS runtime | ||
| 34 | WORKDIR /app | 25 | WORKDIR /app |
| 35 | LABEL org.opencontainers.image.source="https://github.com/RhysSullivan/executor" \ | 26 | LABEL org.opencontainers.image.source="https://github.com/RhysSullivan/executor" \ |
| 36 | org.opencontainers.image.description="Single-container self-hosted Executor" \ | 27 | org.opencontainers.image.description="Single-container self-hosted Executor" \ |
| @@ -39,14 +30,12 @@ ENV NODE_ENV=production \ · 2 unmodified lines | |||
| 39 | EXECUTOR_HOST=0.0.0.0 \ | 30 | EXECUTOR_HOST=0.0.0.0 \ |
| 40 | PORT=4788 \ | 31 | PORT=4788 \ |
| 41 | EXECUTOR_DATA_DIR=/data | 32 | EXECUTOR_DATA_DIR=/data |
| 42 | COPY --from=build /app /app | 33 | COPY --from=prod-deps /usr/local/bin/bun /usr/local/bin/bun |
| 34 | COPY --from=prod-deps /app/.selfhost-runtime /app | ||
| 35 | COPY --from=build /app/apps/host-selfhost/dist /app/apps/host-selfhost/dist | ||
| 43 | WORKDIR /app/apps/host-selfhost | 36 | WORKDIR /app/apps/host-selfhost |
| 44 | RUN mkdir -p /data | ||
| 45 | VOLUME ["/data"] | 37 | VOLUME ["/data"] |
| 46 | EXPOSE 4788 | 38 | EXPOSE 4788 |
| 47 | # Readiness probe against the public /api/health endpoint (a trivial DB ping). | ||
| 48 | HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=5 \ | 39 | HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=5 \ |
| 49 | CMD bun -e "fetch('http://127.0.0.1:4788/api/health').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))" | 40 | CMD bun -e "fetch('http://127.0.0.1:4788/api/health').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))" |
| 50 | # serve.ts binds the Effect AppLayer (API + /mcp + /api/auth + /docs) and serves | 41 | CMD ["bun", "run", "dist-server/serve.js"] |
| 51 | # the built SPA from ./dist — one process. | ||
| 52 | CMD ["bun", "run", "src/serve.ts"] | ||
| @@ -0,0 +1,51 @@ | |||
| 1 | import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs"; | ||
| 2 | import { dirname, join } from "node:path"; | ||
| 3 | import { createRequire } from "node:module"; | ||
| 4 | |||
| 5 | const root = process.cwd(); | ||
| 6 | const out = join(root, ".selfhost-runtime"); | ||
| 7 | const serverOut = join(out, "apps/host-selfhost/dist-server"); | ||
| 8 | const requireFromSelfHost = createRequire(join(root, "apps/host-selfhost/package.json")); | ||
| 9 | |||
| 10 | const externalPackages = [ | ||
| 11 | "quickjs-emscripten", | ||
| 12 | "quickjs-emscripten-core", | ||
| 13 | "@jitl/quickjs-ffi-types", | ||
| 14 | "@jitl/quickjs-wasmfile-release-sync", | ||
| 15 | "@jitl/quickjs-wasmfile-debug-sync", | ||
| 16 | "@jitl/quickjs-wasmfile-release-asyncify", | ||
| 17 | "@jitl/quickjs-wasmfile-debug-asyncify", | ||
| 18 | "@libsql/linux-x64-gnu", | ||
| 19 | ] as const; | ||
| 20 | |||
| 21 | const quickJsExternals = externalPackages.filter( | ||
| 22 | (name) => name === "quickjs-emscripten" || name.startsWith("@jitl/"), | ||
| 23 | ); | ||
| 24 | |||
| 25 | const packageDir = (name: string): string => { | ||
| 26 | const packageJson = requireFromSelfHost.resolve(`${name}/package.json`, { | ||
| 27 | paths: [join(root, "node_modules/.bun/node_modules")], | ||
| 28 | }); | ||
| 29 | return dirname(packageJson); | ||
| 30 | }; | ||
| 31 | |||
| 32 | const copyPackage = (name: string): void => { | ||
| 33 | const destination = join(out, "node_modules", name); | ||
| 34 | mkdirSync(dirname(destination), { recursive: true }); | ||
| 35 | cpSync(packageDir(name), destination, { recursive: true, dereference: true }); | ||
| 36 | }; | ||
| 37 | |||
| 38 | rmSync(out, { recursive: true, force: true }); | ||
| 39 | mkdirSync(serverOut, { recursive: true }); | ||
| 40 | |||
| 41 | await Bun.$`bun build apps/host-selfhost/src/serve.ts --target=bun --format=esm --outdir=${serverOut} ${quickJsExternals.map((name) => `--external=${name}`)}`; | ||
| 42 | |||
| 43 | for (const name of externalPackages) copyPackage(name); | ||
| 44 | |||
| 45 | if (!existsSync(join(serverOut, "serve.js"))) { | ||
| 46 | throw new Error( | ||
| 47 | "Expected bundled self-host server at .selfhost-runtime/apps/host-selfhost/dist-server/serve.js", | ||
| 48 | ); | ||
| 49 | } | ||
| 50 | |||
| 51 | console.log(`Packaged self-host runtime into ${out}`); | ||