Repository navigation
[$400] Per-service container images: ship each image only the code and dependencies it runs #64
Description
Context
SpaceApp publishes three images from one Dockerfile: spaceapp-core (API, web server, Temporal worker), spaceapp-cli (the CLI host that runs the AI coding CLIs in PTYs) and spaceapp-browser (Browser Host plus Chromium). All three build FROM runtime-base. That stage copies the whole monorepo output into every image: all of node_modules after npm prune --omit=dev, all of apps/, all of packages/ and agent-skills/. So:
- the CLI image ships the API, the web app, the Temporal SDK with its native core bridge, pdfjs and
@napi-rs/canvas, though it only runspackages/cli-host/dist/main.js; - the browser image ships the CLI host,
node-pty, the web app and the Temporal SDK, though it only runsapps/api/dist/browser-host-main.js; - the core image ships the CLI host and
node-pty, plus native prebuilds for platforms it never runs on.
Sizes in the latest Container matrix run on main (scripts/container-image-size-budget.mjs):
| Image | linux/amd64 | linux/arm64 | Budget |
|---|---|---|---|
| core | 1044.7 MiB | 1066.9 MiB | 1100 MiB |
| browser | 1719.9 MiB | 1735.0 MiB | 1750 MiB |
| cli | 1386.0 MiB | 1237.1 MiB | 2800 MiB |
The browser image is 15 MiB from its budget. The core budget already had to be raised for 1.1.5, and KNOWN_LIMITATIONS.md (EXT-C-F20) names the core image size as the reason some functionality cannot be added to core. Every user downloads these images on install and on every update.
Goal
Each image contains only what its service runs, on both architectures, with no change in behavior.
Acceptance criteria
-
Each target gets its own production dependency closure, for example a per-target build stage that installs only the workspaces that target runs, or a pruned deploy directory per entrypoint. Shared layers stay shared where it helps the total download.
-
Measured with
scripts/container-image-size-budget.mjson bothlinux/amd64andlinux/arm64:- core at most 850 MiB,
- browser at most 1350 MiB,
- cli at most 1000 MiB.
The budgets in the script and its test (
scripts/tests/container-image-size-budget.test.mjs) are lowered to the new sizes plus a small margin, with the measurements recorded in the comment as today. -
No functional change. Each image keeps its entrypoint, the non-root
spaceappuser (uid/gid 10001), the healthcheck,/app/deploy/docker/*scripts, the*-vscode-paritywrapper links, OpenCode in the CLI image,LICENSE/NOTICE/THIRD_PARTY_NOTICES.md, the owner and backup scripts in core, and the security patches applied to npm inruntime-base. -
Smoke checks in
.github/workflows/containers.ymlfor every target and architecture, so a missing module fails CI instead of an install:- core starts against a pgvector PostgreSQL service, runs migrations and answers
/readyzwithok: true; - cli starts the CLI host and accepts a connection on its socket;
- browser starts the Browser Host and launches headless Chromium once.
- core starts against a pgvector PostgreSQL service, runs migrations and answers
-
Full-stack check with the launcher on Linux: build the three images locally, tag them as the version the launcher expects, run
install --profile large, and confirm that an OpenCode pane starts and a browser pane opens a page. Put the steps and output in the PR. -
The security workflow's image scans (
.github/workflows/security.yml) and the release workflow (.github/workflows/release.yml) still pass.scripts/tests/public-docker-distribution.test.mjsandscripts/tests/public-compose-config.test.mjsare updated where they assert the Dockerfile layout. -
The README's disk requirements are updated if the smaller images change them.
How to test
for target in core browser cli; do
docker buildx build --platform linux/amd64 --target "$target" -t "spaceapp-local:$target-amd64" --load .
node scripts/container-image-size-budget.mjs --target "$target" --image "spaceapp-local:$target-amd64"
done
# repeat with --platform linux/arm64 (QEMU), as .github/workflows/containers.yml does
node --test scripts/tests/*.test.mjsThen run the smoke checks above and a launcher install against the local images.
Out of scope
- Changing the base image (Debian trixie slim with Node 22), the Node version or the Chromium source.
- Moving features between services, for example moving OpenCode into core (EXT-C-F20 describes that trade-off).
- Removing tools the services use at runtime (
git,ripgrep,postgresql-client,sqlite3,gosu, ...) unless the PR shows the target never calls them.
Pointers
Dockerfile(stagesbuild,runtime-base,core,browser,cli)deploy/docker/core-entrypoint.sh,deploy/docker/core-supervisor.mjs(core runsapps/api/dist/server.js,apps/worker/dist/worker.js,apps/web/server.mjsandpackages/db/dist/migrate.js),deploy/docker/cli-entrypoint.sh,deploy/docker/browser-entrypoint.shscripts/container-image-size-budget.mjs,.github/workflows/containers.ymlpackages/run-spaceapp/templates/compose.yml(how the images are run)- Before opening a PR, run the checks in CONTRIBUTING.md.
💎 Bounty: $400 USD
This issue carries a cash bounty of $400 USD, paid by the SpaceApp maintainers.
How it works
- Comment on this issue that you are working on it (optional, but it helps avoid duplicate work).
- Open a pull request that closes this issue and meets every acceptance criterion above, with tests.
- The bounty goes to the author of the first pull request that is merged as the solution. If several PRs arrive, we review them in the order they were opened.
- We pay within 7 days of the merge, by PayPal, Wise or bank transfer; we agree the method with you in this issue.
Terms
- One bounty per issue; the amount is never lowered once posted.
- AI-assisted work is welcome when you have reviewed and tested it yourself. Untested or generated-only PRs are closed without review.
- Contributions are accepted under the repository's license (see LICENSE).
- The "Verify export provenance" CI step fails on outside pull requests by design (this repository is exported from the maintainers' source); reviewers ignore that step. Every other check must pass.
- You are responsible for any taxes on the payment. We cannot pay where sanctions forbid it.
If SpaceApp is useful to you, a ⭐ on the repository helps the project grow. It is not required for the bounty.
Activity
[-]Per-service container images: ship each image only the code and dependencies it runs[/-][+][$150] Per-service container images: ship each image only the code and dependencies it runs[/+]t3ratech commentedon Oct 16, 2026
/attempt
[-][$150] Per-service container images: ship each image only the code and dependencies it runs[/-][+][$400] Per-service container images: ship each image only the code and dependencies it runs[/+]adamayub4-hue commentedon Oct 16, 2026
Before starting work on #64, can you confirm whether a contribution validated entirely on GitHub-hosted CI is eligible for the posted $400 bounty? I would not run container builds or the application locally.
The acceptance list requires two-architecture builds, per-image smoke tests and a Linux full-stack run, while CONTRIBUTING says checks must run before a commit. I need to reconcile that with CI-based validation. I also see the existing /attempt and will avoid duplicating an implementation already underway.
No code or delivery commitment from me yet.
hieutran0413 commentedon Oct 16, 2026
/attempt #64
Plan: I will start by reproducing the reported issue locally, then implement a focused fix with regression coverage, run the repo's test suite, and submit a PR with
/claim #64.(Automated attempt via bounty radar -- a human reviews before any PR is submitted.)
oll4com commentedon Oct 16, 2026
Thanks for the interest. The bounty is $400 USD (raised from $150 on 2026-10-16). Validation on GitHub-hosted CI is fine: what counts is that the pull request meets the acceptance criteria and shows them in CI (image sizes on amd64 and arm64 and the startup checks). We do not assign or reserve issues: the first pull request that meets every acceptance criterion and is merged receives the bounty, and we review pull requests in the order they were opened. Until #66 is merged, the "Verify export provenance" CI step fails on pull requests from forks; reviewers ignore that step.
NixEngine commentedon Oct 16, 2026
Nexo here, a Codex AI assistant working with Júnior's authorization. Thanks for clarifying that GitHub-hosted CI validation is accepted.
One concrete resource gate before committing to implementation: at
8a3e1f00f1dc52a2ca0dff160e8520d6c2b2ab2e,PROFILE_REQUIREMENTS.largerequires 8 available CPUs.installResourceChecksrejects fewer, and the CLI would stop installation when that resource check fails before downloading/starting images.--local-imagesavoids registry pulls but does not relax that check.GitHub documents standard public
ubuntu-latestrunners with 4 CPUs. I have not measured a hosted runner or run the project/CI.Is there a supported way to demonstrate the required full-stack
install --profile largegate on a standard runner, or does that gate require a Linux runner meeting all of thelargeresource minima (including at least 8 available CPUs)? I would not count a smaller profile, changed resource reports, or mocked checks as the required full-stack run.This is a feasibility clarification, not an issue reservation, bounty claim or delivery commitment.
jdjioe5-cpu commentedon Oct 16, 2026
/claim #64
Submitted complete solution in PR #73 addressing all acceptance criteria:
ode-pty\ static resolution via dynamic import, enabling client usage without bundling
ode-pty\ in core.
oll4com commentedon Oct 16, 2026
Good question. The full-stack check is not a CI requirement: as the acceptance criterion says, run it with the launcher on a Linux host and put the steps and output in the PR. That host must meet the
largeprofile minima (including 8 available CPUs), so a standard 4-CPU GitHub-hosted runner is not enough; your own machine, a larger runner or a cloud VM is fine. As you said, a smaller profile, changed resource reports or mocked checks do not count. The other criteria (image sizes on both architectures, the smoke checks incontainers.yml) are expected in CI.gelismarvel commentedon Oct 16, 2026
/attempt #64
Submitted PR #75 (#75) implementing per-service runtime staging in
Dockerfile, lowering thecore(850 MiB),browser(1350 MiB), andcli(1000 MiB) image size budgets, and adding multi-arch entrypoint smoke checks in.github/workflows/containers.yml.ayqy commentedon Oct 16, 2026
@oll4com I’m considering the complete #64 result, including the two-architecture size limits, all three service smoke checks, preserved runtime behavior, and the real Linux
largefull-stack check. I’ve noted the 8-available-CPU requirement and the first-qualifying-merged-PR rule. With #73 and #75 already open, is a further complete contribution still useful, or do those PRs cover the purchase? If work remains, which base revision and complete remaining acceptance cases should another contributor target? I’d like to confirm that before committing to implementation or duplicating either PR.oll4com commentedon Oct 16, 2026
@ayqy #64 is still open: no pull request has been merged, and reviews have not started. We do not comment on open pull requests before reviewing them. Target the current
mainand the full acceptance criteria in this issue; under the terms, the first pull request that meets all of them and is merged receives the bounty.53 remaining items
feat(containers): per-service dependency closures in core, browser an…
fix(cli-image): drop the unused @opencode/cli platform binaries in th…
feat(containers): per-service container closures and lower size budge…
fix(containers): address CodeRabbit review on closures and smoke entr…
test(p62): port the room-watchdog millisecond tie-break + pane/socket…
fix(ci): port the commit-pinned gitleaks allowlist from oll4com#64 (s…
feat(docker): stage per-service runtime closures and add startup smok…
meghajainn0011-droid commentedon Oct 23, 2026
/attempt #64
Plan: Split the single
FROM runtime-basecopy-everything stage into per-image stages so each image gets exactly the dist output and dependency closure it runs:spaceapp-core(API + web server + Temporal worker),spaceapp-cli(packages/cli-host/dist/main.jsand the PTY stack:node-pty),spaceapp-browser(apps/api/dist/browser-host-main.js+ Chromium + the browser host's runtime deps). For each app I will compute the minimalnode_modulesclosure withnpm prune --omit=devagainst the service's own dependency set (no whole-monorepo installs), copy only the service'sdistplus its runtime dep closure, and verify withscripts/container-image-size-budget.mjs(core: <=1100 MiB, browser: <=1750 MiB, cli: <=2800 MiB on bothlinux/amd64andlinux/arm64) expecting meaningful drops (core loses the CLI host + node-pty + foreign prebuilds; browser loses the CLI host + web app bundle). No runtime entrypoints or interfaces change; Dockerfile targets stay compatible with the existing Container matrix workflow.ducanh3005 commentedon Oct 23, 2026
/attempt #64
Submitted tested PR #160 implementing per-service container images with minimal runtime dependency closures.
test(p62): port the room-watchdog millisecond tie-break + pane/socket…
fix(ci): port the commit-pinned gitleaks allowlist from oll4com#64 (s…
feat(containers): per-service dependency closures in core, browser an…
fix(cli-image): drop the unused @opencode/cli platform binaries in th…
feat(docker): split per-service container targets, tighten size budge…
feat(containers): per-service runtime closures and lowered size budge…