* 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. | |
| 2 | −# Build context is the REPO ROOT (the bun workspace install needs every member): | |
| 1 | +# Self-hosted Executor, single container, no external services. | |
| 2 | +# Build context is the repo root because Bun workspaces need every member: | |
| 3 | 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 | 6 | # docker run -p 4788:4788 -e BETTER_AUTH_SECRET=$(openssl rand -hex 32) \ |
| 7 | 7 | # -e EXECUTOR_BOOTSTRAP_ADMIN_EMAIL=you@example.com \ |
| 8 | 8 | # -e EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD=... \ |
| 9 | 9 | # -e EXECUTOR_WEB_BASE_URL=https://your.domain \ |
| 10 | 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 | 18 | FROM oven/bun:1 AS build |
| 17 | 19 | WORKDIR /app |
| 18 | 20 | COPY . . |
| 19 | −# Full install (dev deps included — vite/turbo/plugins are needed to build). | |
| 20 | 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 | 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 ──────────────────────────── | |
| 33 | −FROM oven/bun:1 AS runtime | |
| 24 | +FROM gcr.io/distroless/cc-debian12 AS runtime | |
| 34 | 25 | WORKDIR /app |
| 35 | 26 | LABEL org.opencontainers.image.source="https://github.com/RhysSullivan/executor" \ |
| 36 | 27 | org.opencontainers.image.description="Single-container self-hosted Executor" \ |
| @@ -39,14 +30,12 @@ ENV NODE_ENV=production \ · 2 unmodified lines | ||
| 39 | 30 | EXECUTOR_HOST=0.0.0.0 \ |
| 40 | 31 | PORT=4788 \ |
| 41 | 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 | 36 | WORKDIR /app/apps/host-selfhost |
| 44 | −RUN mkdir -p /data | |
| 45 | 37 | VOLUME ["/data"] |
| 46 | 38 | EXPOSE 4788 |
| 47 | −# Readiness probe against the public /api/health endpoint (a trivial DB ping). | |
| 48 | 39 | HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=5 \ |
| 49 | 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 | |
| 51 | −# the built SPA from ./dist — one process. | |
| 52 | −CMD ["bun", "run", "src/serve.ts"] | |
| 41 | +CMD ["bun", "run", "dist-server/serve.js"] | |
| @@ -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}`); | |