Skip to content

Use RUN --mount for uv to cut image size from 114 MB to 64 MB#437

Merged
nitrobass24 merged 8 commits into
developfrom
chore/uv-mount-optimization
May 1, 2026
Merged

Use RUN --mount for uv to cut image size from 114 MB to 64 MB#437
nitrobass24 merged 8 commits into
developfrom
chore/uv-mount-optimization

Conversation

@nitrobass24
Copy link
Copy Markdown
Owner

@nitrobass24 nitrobass24 commented May 1, 2026

Summary

Three optimizations to fix the size regression from PR #436 and further reduce image size:

  1. Restore 3-stage build — The 2-stage collapse caused a regression (45 MB → 114 MB) because rm -rf in a RUN layer can't reclaim space from the base image's Python layer. The intermediate python-deps stage ensures COPY --from only brings stripped files into the final alpine:3.23 image.

  2. Use RUN --mount for uv — Mount the uv binary during RUN instead of COPY-ing it into a layer. The old COPY + rm pattern left a 49 MB ghost layer even though uv was deleted afterward. Applied to both production and test-image Dockerfiles.

  3. Strip additional Python artifacts — Remove C headers (2.4 MB), build config (284 KB), REPL enhancements (240 KB), decimal fallback (224 KB), test extension modules (204 KB), and ~100 unused encoding codepages (~1.2 MB).

Image size: 114 MB → 44.3 MB (was 45 MB before PR #436)

Test plan

  • make build builds successfully
  • Image size verified at 44.3 MB
  • Container starts and serves on port 8800
  • CI passes (Docker build + E2E tests)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Optimized Docker runtime image builds to reduce footprint and improve performance through multi-stage configuration and aggressive cleanup of unnecessary runtime components.
    • Improved Docker build cache efficiency by excluding additional cache directories from the build context.
    • Enhanced Docker test environment setup with improved build tool integration for dependency installation.

Mount the uv binary from ghcr.io/astral-sh/uv:0.11 during RUN instead
of COPY-ing it into a layer. The COPY+delete pattern left a 49.4 MB
layer in the image even though uv was removed afterward. The mount
approach keeps uv out of all committed layers entirely.

Image size: 114 MB → 64 MB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 846a8f12-75ff-4a93-a767-5973db7b83be

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The pull request refactors Docker build configurations to reduce image size. The main runtime Dockerfile implements multi-stage builds with aggressive Python standard library stripping, switches to Alpine 3.23 base, and expands cleanup steps. The test Dockerfile uses BuildKit mounts to access the uv binary instead of copying it. A Ruff cache ignore rule is added to the build context.

Changes

Cohort / File(s) Summary
Multi-stage Docker build optimization
src/docker/build/docker-image/Dockerfile, src/docker/build/test-image/Dockerfile
Refactored Python dependency installation via multi-stage builds and BuildKit mounted binaries. Main image migrated to Alpine 3.23 with Python stdlib/artifact stripping in dedicated python-deps stage. Test image replaced binary copy with mount-based access.
Build context filtering
src/docker/build/docker-image/Dockerfile.dockerignore
Added exclusion rule for Ruff cache directories from Docker build context.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly describes the main optimization: using RUN --mount for uv to reduce image size from 114 MB to 64 MB. This aligns with the primary change in the changeset across both Dockerfiles.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/uv-mount-optimization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

nitrobass24 and others added 6 commits April 30, 2026 19:24
The 2-stage collapse in PR 3 caused a size regression (45 MB → 114 MB)
because stdlib cleanup in a RUN layer can't reclaim space from the base
image's Python layer. Restore the intermediate python-deps stage so
COPY --from only brings stripped files into the final alpine image.

Combined with RUN --mount for uv (avoids 49 MB ghost layer), the final
image is 48 MB — smaller than the original 45 MB thanks to the Alpine
3.23 upgrade and Python 3.13 stdlib removals.

Image size: 114 MB → 48 MB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove C headers (/usr/local/include, 2.4 MB), build config
(config-3.13-*, 284 KB), REPL enhancements (_pyrepl, 240 KB),
decimal fallback (_pydecimal.py, 224 KB), test extension modules
(xx*.so, 204 KB), and ~100 unused encoding codepages (~1.2 MB).

Keeps only UTF-8, ASCII, Latin-1, UTF-16/32, charmap, IDNA, and
unicode escape encodings — all that LC_ALL=C.UTF-8 needs.

Image size: 48 MB → 44 MB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Strip the package manager (apk, libapk, /etc/apk, /lib/apk) and
unused openssh binaries (ssh-keyscan, ssh-add, ssh-agent,
ssh-pkcs11-helper, ssh-copy-id) plus utility binaries (scanelf,
c_rehash, iconv, getconf, ssl_client, findssl.sh) from the final
image. The app only needs ssh, scp, and sftp.

Prevents installing packages inside a running container and reduces
attack surface.

Image size: 44 MB → 42.5 MB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove /etc/ssh/moduli (568 KB) — only needed by sshd (server), not
the ssh client. Add .ruff_cache to .dockerignore to exclude 1.3 MB of
linter cache from COPY src/python/.

Image size: 42.5 MB → 41.5 MB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add .pytest_cache, pyrightconfig.json to .dockerignore. Remove
pyproject.toml and uv.lock from /app/python/ after COPY (they're
needed in the python-deps stage but not at runtime).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The typings/ directory contains .pyi type stubs used only by Pyright
during development. Not needed at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/docker/build/docker-image/Dockerfile`:
- Line 17: Change the floating Alpine tag used in the build stage to the
explicit alpine3.23 variant to match the final stage: replace the FROM line that
reads "FROM python:3.13-alpine" with "python:3.13-alpine3.23" and do the same
for the second occurrence later in the Dockerfile (the second builder stage
around line 72) so both builder stages are pinned to Alpine 3.23 and avoid
future musl/alpine mismatches.

In `@src/docker/build/test-image/Dockerfile`:
- Around line 6-8: The RUN line invoking the vendored uv pip installer is
passing --group test without qualifying the target pyproject, so the test group
from /tmp/pyproject.toml is not being installed; update the uv pip install
invocation in the RUN step (the line that starts with "/tmp/uv pip install") to
qualify the pyproject, e.g. use "--group /tmp/pyproject.toml:test" or switch to
"--project /tmp --group test" so the test dependencies from /tmp/pyproject.toml
are installed correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dd0f53b0-671e-4260-8c40-a3140d768ea4

📥 Commits

Reviewing files that changed from the base of the PR and between 51ede44 and 12b8113.

📒 Files selected for processing (3)
  • src/docker/build/docker-image/Dockerfile
  • src/docker/build/docker-image/Dockerfile.dockerignore
  • src/docker/build/test-image/Dockerfile


# Install Python dependencies with uv, then clean up
COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /usr/local/bin/uv
FROM python:3.13-alpine AS python-deps
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "Dockerfile" -path "*/docker/build/docker-image/*"

Repository: nitrobass24/seedsync

Length of output: 107


🏁 Script executed:

cat -n src/docker/build/docker-image/Dockerfile

Repository: nitrobass24/seedsync

Length of output: 6703


🌐 Web query:

python:3.13-alpine3.23 docker hub tag availability

💡 Result:

Yes, the python:3.13-alpine3.23 tag is available on Docker Hub for the official Python Docker image. It is based on Alpine Linux 3.23 and uses Python 3.13.13. You can pull it with docker pull python:3.13-alpine3.23. This tag is explicitly listed among supported tags on the official Docker Hub page, has dedicated image layer details pages, and a corresponding Dockerfile in the docker-library/python repository at path 3.13/alpine3.23/Dockerfile.

Citations:


🌐 Web query:

python docker official image alpine3.23 tag variants 3.13

💡 Result:

The official Python Docker image provides Python 3.13 on Alpine 3.23 via the following tags (latest as of 2026-05-01, pushed 4 days ago): - python:3.13.13-alpine3.23 - python:3.13-alpine3.23 (aliases to the above) These are the standard "alpine" variants. There are no additional sub-variants (e.g., slim-alpine) specifically for Alpine 3.23 with Python 3.13; the official variants are primarily distinguished by base OS/distribution (Debian full/slim, Alpine, Windows Server Core). The image is based on alpine:3.23, builds Python 3.13.13 from source (verified SHA256 checksum), includes pip/ensurepip, and supports multiple architectures (amd64, arm/v6/v7/64, 386, ppc64le, riscv64, s390x). Compressed size ~16MB per arch. To pull: docker pull python:3.13-alpine3.23 Source Dockerfiles: https://github.com/docker-library/python/tree/master/3.13/alpine3.23

Citations:


Pin the builder to alpine3.23 explicitly to prevent Alpine/musl version mismatch on future rebuilds.

python:3.13-alpine is a floating tag that currently aliases to alpine3.23. The final stage is pinned to alpine:3.23, but leaving the builder on the floating alias risks an Alpine/musl mismatch when Docker's shared tag moves to a future version. Use python:3.13-alpine3.23 to keep both stages coupled to the same Alpine version.

Also applies to line 72.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/docker/build/docker-image/Dockerfile` at line 17, Change the floating
Alpine tag used in the build stage to the explicit alpine3.23 variant to match
the final stage: replace the FROM line that reads "FROM python:3.13-alpine" with
"python:3.13-alpine3.23" and do the same for the second occurrence later in the
Dockerfile (the second builder stage around line 72) so both builder stages are
pinned to Alpine 3.23 and avoid future musl/alpine mismatches.

Comment on lines +6 to 8
RUN --mount=from=ghcr.io/astral-sh/uv:0.11,source=/uv,target=/tmp/uv \
/tmp/uv pip install --system --no-cache --strict \
-r /tmp/pyproject.toml --group test \
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "Dockerfile" -path "*/test-image/*" | head -5

Repository: nitrobass24/seedsync

Length of output: 105


🏁 Script executed:

cat -n src/docker/build/test-image/Dockerfile | head -20

Repository: nitrobass24/seedsync

Length of output: 740


🌐 Web query:

uv pip install --group documentation astral-sh uv pyproject.toml

💡 Result:

The command uv pip install --group documentation astral-sh uv pyproject.toml installs the astral-sh/uv package (and potentially others specified) into the current virtual environment, sourcing the dependency group named "documentation" from the pyproject.toml file in the current directory. This works if your local pyproject.toml defines a [dependency-groups.documentation] section with astral-sh/uv (or other packages) listed as dependencies. For example: [dependency-groups] documentation = ["astral-sh/uv"] The --group documentation flag tells uv pip install to include dependencies from that named group in the current project's pyproject.toml. The pyproject.toml argument specifies the file containing the group definition (defaults to ./pyproject.toml if omitted). Packages like astral-sh/uv refer to the uv package from the Astral GitHub repository. Note: --group only applies to groups from the specified/local pyproject.toml; it does not affect other install sources. This is distinct from uv add --group documentation astral-sh/uv, which would add the package to the group in pyproject.toml itself. To use: Ensure you're in a uv-managed environment (uv venv or project dir), then run the command. It will resolve and install the locked versions from uv.lock if present.

Citations:


🌐 Web query:

uv pip install -r flag requirements file group option

💡 Result:

To install packages from a requirements file using uv's pip-compatible interface, use uv pip install -r requirements.txt. This works exactly like pip install -r requirements.txt. The --group flag in uv pip install installs dependencies from a specified dependency group in a pyproject.toml file (PEP 735), not from the requirements file. Syntax: --group [path:]name, e.g., uv pip install --group dev installs the dev group from the current pyproject.toml, or uv pip install --group other/pyproject.toml:dev for a specific file. Important: --group does not apply to requirements files passed with -r. They are independent: uv pip install -r requirements.txt --group dev installs from both sources separately. If no pyproject.toml or group exists, --group will error. For project-managed dependency groups (preferred in uv), use uv sync --group dev instead of uv pip install. This feature was added in early 2025 for pip compatibility (sources confirm implementation and docs).

Citations:


🌐 Web query:

uv pip install pyproject.toml vs -r requirements.txt difference

💡 Result:

The command "uv pip install pyproject.toml" installs dependencies listed directly in a pyproject.toml file using uv's pip-compatible interface, treating the file as a requirements source similar to requirements.txt. However, this is not the recommended uv workflow for projects. Key differences: - uv pip install -r requirements.txt: Installs packages from a flat list in requirements.txt into the current environment (requires existing venv). requirements.txt typically contains pinned versions (e.g., from pip freeze or pip compile) and lacks project metadata like Python version or optional deps. - uv pip install pyproject.toml: Similar, but reads deps from [project.dependencies] or [dependency-groups] in pyproject.toml. Still pip-style, no lockfile integration, no automatic project env management. Recommended uv project workflow (superior to both): - Use "uv sync" in a directory with pyproject.toml (+ uv.lock): Locks deps to exact versions in uv.lock, installs into .venv, syncs environment automatically. Handles metadata, dev deps, Python version. Run scripts with "uv run". - To add deps: "uv add pkg" (updates pyproject.toml + lockfile + env). - Migrate from requirements.txt: "uv init", then "uv add -r requirements.txt". pyproject.toml is for project metadata/declarative deps; requirements.txt is legacy pinned list. uv prefers pyproject.toml + uv.lock for reproducibility and speed. "uv pip install pyproject.toml" works but bypasses uv project features like auto-sync and locking.

Citations:


--group test is targeting the wrong project file.

With uv pip install, --group does not apply to the source passed via -r; it must be qualified with a path to specify the target pyproject.toml file. In this step, the test group is not being sourced from /tmp/pyproject.toml. Use --group /tmp/pyproject.toml:test or --project /tmp --group test to install the test dependencies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/docker/build/test-image/Dockerfile` around lines 6 - 8, The RUN line
invoking the vendored uv pip installer is passing --group test without
qualifying the target pyproject, so the test group from /tmp/pyproject.toml is
not being installed; update the uv pip install invocation in the RUN step (the
line that starts with "/tmp/uv pip install") to qualify the pyproject, e.g. use
"--group /tmp/pyproject.toml:test" or switch to "--project /tmp --group test" so
the test dependencies from /tmp/pyproject.toml are installed correctly.

Add to python-deps cleanup: _tkinter.so (tkinter dir was removed but
not the .so), _xxtestfuzz.so (test module), _lsprof.so (profiler),
_interp*.so (sub-interpreters), and venv/ directory.

Add /usr/lib/libapk.so* to runtime cleanup (missed in apk removal).

Image size: 41.3 MB → 40.9 MB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nitrobass24 nitrobass24 merged commit cf856e6 into develop May 1, 2026
17 checks passed
@nitrobass24 nitrobass24 deleted the chore/uv-mount-optimization branch May 1, 2026 01:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant