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.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-selfhost10# -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 ──────────────────────12FROM oven/bun:1 AS prod-deps
13WORKDIR /app
14COPY . .
15RUN bun install --frozen-lockfile --production --ignore-scripts --filter @executor-js/host-selfhost \
16 && bun run apps/host-selfhost/scripts/package-runtime.ts
17
16FROM oven/bun:1 AS build18FROM oven/bun:1 AS build
17WORKDIR /app19WORKDIR /app
18COPY . .20COPY . .
19# Full install (dev deps included — vite/turbo/plugins are needed to build).
20RUN bun install --frozen-lockfile21RUN bun install --frozen-lockfile
21# Builds @executor-js/vite-plugin (via turbo) then the self-host SPA into
22# apps/host-selfhost/dist.
23RUN cd apps/host-selfhost && bun run build22RUN 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.
30RUN rm -rf node_modules && bun install --frozen-lockfile --production --ignore-scripts
31 23
32# ── Runtime stage: serve the built app under Bun ────────────────────────────24FROM gcr.io/distroless/cc-debian12 AS runtime
33FROM oven/bun:1 AS runtime
34WORKDIR /app25WORKDIR /app
35LABEL org.opencontainers.image.source="https://github.com/RhysSullivan/executor" \26LABEL 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=/data32 EXECUTOR_DATA_DIR=/data
42COPY --from=build /app /app33COPY --from=prod-deps /usr/local/bin/bun /usr/local/bin/bun
34COPY --from=prod-deps /app/.selfhost-runtime /app
35COPY --from=build /app/apps/host-selfhost/dist /app/apps/host-selfhost/dist
43WORKDIR /app/apps/host-selfhost36WORKDIR /app/apps/host-selfhost
44RUN mkdir -p /data
45VOLUME ["/data"]37VOLUME ["/data"]
46EXPOSE 478838EXPOSE 4788
47# Readiness probe against the public /api/health endpoint (a trivial DB ping).
48HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=5 \39HEALTHCHECK --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 serves41CMD ["bun", "run", "dist-server/serve.js"]
51# the built SPA from ./dist — one process.
52CMD ["bun", "run", "src/serve.ts"]
apps/host-selfhost/scripts/package-runtime.ts+51−0
@@ -0,0 +1,51 @@
1import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
2import { dirname, join } from "node:path";
3import { createRequire } from "node:module";
4
5const root = process.cwd();
6const out = join(root, ".selfhost-runtime");
7const serverOut = join(out, "apps/host-selfhost/dist-server");
8const requireFromSelfHost = createRequire(join(root, "apps/host-selfhost/package.json"));
9
10const 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
21const quickJsExternals = externalPackages.filter(
22 (name) => name === "quickjs-emscripten" || name.startsWith("@jitl/"),
23);
24
25const 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
32const 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
38rmSync(out, { recursive: true, force: true });
39mkdirSync(serverOut, { recursive: true });
40
41await Bun.$`bun build apps/host-selfhost/src/serve.ts --target=bun --format=esm --outdir=${serverOut} ${quickJsExternals.map((name) => `--external=${name}`)}`;
42
43for (const name of externalPackages) copyPackage(name);
44
45if (!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
51console.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