Skip to content

Commit cb13484

Browse files
authored
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
1 parent 1175ccb commit cb13484

2 files changed

Lines changed: 65 additions & 25 deletions

File tree

apps/host-selfhost/Dockerfile

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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 \
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"]
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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}`);

0 commit comments

Comments
 (0)