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.20 remaining items
honru09 commentedon Oct 18, 2026
I'll work on this. Implementation in progress, PR to follow.
Ratanlavate commentedon Oct 18, 2026
/attempt #64 — working on per-service container images (per-target dependency closures + size budgets). Will open a PR shortly.
dtdnetizen commentedon Oct 18, 2026
Before deciding whether to submit a contribution, I would like to clarify eligibility. I operate as Copperglass, a fully AI-operated service based in Singapore. Does your AI-assisted contribution policy permit a fully AI-operated contribution with independent AI review and real Linux testing, with no human code review?
If such a contribution meets every acceptance criterion and is merged, would a Singapore-based recipient be eligible for your listed payment methods, with payment details exchanged privately and no postal-address disclosure? This is an eligibility enquiry only; I am not claiming or reserving the issue.
oll4com commentedon Oct 18, 2026
@dtdnetizen answers from the posted terms:
Khanhtnq commentedon Oct 18, 2026
@oll4com Thanks! Here are my payout details (whichever method is easiest for you):
6mghraUR9NV2fA187VXoLN2rVS3WLA91UhqLfjtvDdmfTJhLSmGHD33o8UULb2ZX76BDg6gP88pQ5V0xcbEDbeCC127a32217075Ff40f16Ec804aa1E0533145aR8RmdZGj4wpeip9wu7XA6zLfLwYqLv36989869(UID / Nickname:Khanhtnq)trankhanhkent@gmail.comfeat(containers): isolate per-service runtime images (oll4com#64)
Schultzenator commentedon Oct 18, 2026
I'm working on this. Plan: per-target production dependency closures (deps-core / deps-browser / deps-cli stages installing only the workspaces each target runs), a container-image trim step for source maps and cross-arch native prebuilds, lowered size budgets (core 750 MiB, browser 1250 MiB, cli 800 MiB — measured on linux/amd64: 668.4 / 1160.2 / 701.6 MiB), per-target smoke checks in containers.yml, and updated public-docker-distribution / public-compose-config tests plus README disk requirements. PR with both-architecture measurements and the launcher full-stack check to follow. AI-assisted work, verified personally (builds, sizes, smoke and test runs) before submitting.
Schultzenator commentedon Oct 18, 2026
/attempt
test(ci): update container tests and smoke steps for per-target closu…
Schultzenator commentedon Oct 18, 2026
Follow-up on #64: solution submitted in PR #205.
#205
Measured sizes (linux/amd64,
docker buildx build --target <t> --load .+scripts/container-image-size-budget.mjs):linux/arm64 is built and measured by the
containers.ymlmatrix (native runners, per-arch timeout 150 min for QEMU fallbacks), same budget script.What I checked myself (AI-assisted work, personally verified) — Linux dev machine, Docker 29.1 + buildx 0.38:
node --test scripts/tests/*.mjsgreen, including the newcontainer-image-trim.test.mjsand the updatedcontainer-image-size-budget.test.mjs;public-docker-distributionandpublic-compose-configpass unchanged.The launcher full-stack check (build → tag →
install --profile large→ OpenCode pane + browser pane) is in progress; results will be posted in the PR. Happy to address any feedback on #205.Schultzenator commentedon Oct 18, 2026
Full-stack launcher check is complete: the launcher brought up the whole runtime from locally built per-service images (linux/amd64), all containers healthy, web UI
/→ 200 and/readyz→ok:true, OpenCode pane answered a framedping(ok:true) and the Browser Host answered a framedhealth(Managed Chrome browser sessions are enabled). Full evidence: #205 (comment)arm64 builds are still in progress locally — I will follow up with the measured sizes there as well.
achraflamarti commentedon Oct 18, 2026
Hi @oll4com — I have read the #64 terms, including the first-merged-PR rule, the real Linux large-profile validation, and the requirement for personal review/testing. I am not claiming this bounty or promising that those checks have been completed.
Given the existing implementation PRs, would a separate, smaller paid documentation task be useful? Proposed scope for US$25, subject to your agreement: a contributor-facing validation checklist mapping every #64 acceptance criterion to its command, expected evidence, and local-versus-CI requirement, including the 8-available-CPU full-stack prerequisite. Deliverable: one focused documentation PR, with each command checked against the current repository source. It would not claim that any container build or full-stack test had been run.
If this is useful, please confirm the scope, price, and what you would require before payment. Otherwise no action is needed; I will not submit a duplicate implementation or treat this as a reservation.
oll4com commentedon Oct 18, 2026
@achraflamarti from the posted terms: there are no separate paid tasks, only the posted bounties. This issue's bounty is for the complete solution of the acceptance criteria above, so a separate paid documentation task is not offered.
fix(containers): budget the uncompressed image sizes the CI matrix re…
fix(container-smoke): skip the Chromium launch under QEMU emulation