History

Optimize self-host Docker image size (#1068)

* 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

Rhys Sullivan committed 4 months ago

Commit cb134841 parent 1175ccb2 files · +65 −25Browse files

Unified · Split

2 files
  1. apps/host-selfhost/Dockerfile +14 −25
  2. apps/host-selfhost/scripts/package-runtime.ts +51 −0
apps/host-selfhost/Dockerfile+14−25
@@ -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:
33 # docker build -f apps/host-selfhost/Dockerfile -t executor-selfhost .
44 #
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:
66 # docker run -p 4788:4788 -e BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
77 # -e EXECUTOR_BOOTSTRAP_ADMIN_EMAIL=you@example.com \
88 # -e EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD=... \
99 # -e EXECUTOR_WEB_BASE_URL=https://your.domain \
1010 # -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.
1411
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+
1618 FROM oven/bun:1 AS build
1719 WORKDIR /app
1820 COPY . .
19−# Full install (dev deps included — vite/turbo/plugins are needed to build).
2021 RUN bun install --frozen-lockfile
21−# Builds @executor-js/vite-plugin (via turbo) then the self-host SPA into
22−# apps/host-selfhost/dist.
2322 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
3123
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
3425 WORKDIR /app
3526 LABEL org.opencontainers.image.source="https://github.com/RhysSullivan/executor" \
3627 org.opencontainers.image.description="Single-container self-hosted Executor" \
@@ -39,14 +30,12 @@ ENV NODE_ENV=production \ · 2 unmodified lines
3930 EXECUTOR_HOST=0.0.0.0 \
4031 PORT=4788 \
4132 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
4336 WORKDIR /app/apps/host-selfhost
44−RUN mkdir -p /data
4537 VOLUME ["/data"]
4638 EXPOSE 4788
47−# Readiness probe against the public /api/health endpoint (a trivial DB ping).
4839 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=5 \
4940 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"]
apps/host-selfhost/scripts/package-runtime.ts+51−0
@@ -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}`);

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t