Skip to content

perf(build): slim Docker image 1.63GB → 0.81GB (-50.31%) - #999

Open
cmd-err wants to merge 1 commit into
juspay:releasefrom
cmd-err:feat/docker-image-optimization
Open

perf(build): slim Docker image 1.63GB → 0.81GB (-50.31%)#999
cmd-err wants to merge 1 commit into
juspay:releasefrom
cmd-err:feat/docker-image-optimization

Conversation

@cmd-err

@cmd-err cmd-err commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Minimal-change Dockerfile refactor. Drops registry image size 1,705 MB → 857 MB (-50%) on identical Cloud Build hardware, with zero changes to pyproject.toml, uv.lock, app code, runtime CMD semantics, or external build contracts.

Change surface

One file: Dockerfile. No pyproject.toml / uv.lock / .dockerignore / workflow / app-code changes.

What changed

  1. Replace google-cloud-sdk install with plain curl + Bearer token. Old code installed the full GCloud SDK (~845 MB unpacked / ~170 MB compressed) just to run gcloud storage cp to pull 3 AIC model files. New code hits https://storage.googleapis.com/<bucket>/<path> with Authorization: Bearer $TOKEN where the token comes from the same gcp_token BuildKit secret.curl is already in the apt layer, so this is a net -845 MB of disk with zero added bytes.

  2. Move useradd before the big COPY, then COPY --chown=appuser:appuser . .. Old code did RUN chown -R appuser:appuser /app after COPY . ., which forced Docker to duplicate every byte in /app (1.82 GB unpacked / ~500 MB compressed) into a second layer. New code never duplicates the app dir.

  3. Delete the uv cache inside the same layer that fills it. Two places: after uv sync and after the nltk.downloader step. Layers are additive — removing the cache in a later layer just adds a whiteout while the bytes still ship in the build layer. Net: -861 MB unpacked from the uv sync layer alone.

  4. uv run --no-sync python run.py as CMD. Without --no-sync, uv run re-resolves the project on every container start, tries to reinstall the project wheel + dev dependencies, and fails writing into the root-owned /app/.venv (which is intentionally not chown'd — the venv only needs read+execute). With --no-sync it runs the already-built environment as-is, so we can keep /app/.venv root-owned and still avoid a 1.8 GB chown -R layer.

Verified head-to-head on identical Cloud Build (E2_HIGHCPU_8, same source tree)

Baseline This PR
image tag voice-agent:test-baseline voice-agent:test-chown-fixed
gcloud artifacts ... --include-tags SIZE 1,705 MB 857 MB
Cloud Build wall time 7m17s 5m33s
AIC models inside /app/models/voice/aic/ ✓ 89 MB ✓ 89 MB (same files)
/root/google-cloud-sdk/ 845 MB absent
Pod boot: /health returns 200 n/a OK (~30s native amd64)
Pipecat services import n/a OK
Subprocess-spawned voice bot (daily.py pattern) n/a OK

Backward compatibility — everything unchanged

  • Same BuildKit secret gcp_token (same contract as original).
  • Same OAuth token source in CI: gcloud auth print-access-token passed via --secret.
  • Same AIC_BUCKET_PATH and AIC_MODELS env/ARG plumbing.
  • Same CMD shape (uv run python run.py); only added --no-sync. No process supervision change.
  • No Python deps added/removed/upgraded. uv.lock untouched.
  • No .dockerignore change. COPY . . still copies the same context.
  • No apt-package removed from runtime, no new apt-package added.
  • AWS deployment path unchanged: no gcp_token secret → models step is a no-op.
  • All layering/permission invariants preserved: app still runs as appuser, /usr/local/nltk_data still owned by appuser, /app/.uv-cache writable by appuser.

Files

  • Dockerfile (+49 / -23)

Out of scope (intentionally)

  • Multi-stage build (would save another ~150 MB by keeping build-essential etc. out of runtime) — needs careful confirmation that no transitive Python package re-compiles at runtime.
  • Dependency trimming (pipecat[azure], redis[hiredis], pytorch) — unrelated, deferred.
  • K8s-side image warmer / autoscaling changes — separate RCA phases.

Co-authored-by: Claude noreply@anthropic.com

Copilot AI lite review requested due to automatic review settings August 11, 2026 05:37
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR replaces the single-stage Docker build with separate dependency, model-fetch, and runtime stages. It adds GCS-based AIC model retrieval, narrows runtime dependencies, updates Python dependencies, and expands Docker ignore rules.

Changes

Container build pipeline

Layer / File(s) Summary
Dependency and build-context preparation
.dockerignore, Dockerfile, pyproject.toml
The builder creates an isolated virtual environment and installs locked dependencies. Docker excludes local assets and caches. pipecat-ai and Redis extras are reduced.
AIC model retrieval
scripts/fetch_aic_models.py, Dockerfile
The build downloads configured AIC models from Google Cloud Storage using a BuildKit token or ADC. Missing credentials do not fail the script.
Minimal runtime assembly
Dockerfile
The runtime image installs only runtime libraries, copies the prepared environment and models, runs as appuser, copies selected application paths, and starts run.py with the venv Python interpreter.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: swaroopvarma2359

Poem

A rabbit packs the models tight,
Into a lean image overnight.
The build steps hop from stage to stage,
While GCS fills the model cage.
Non-root paws then start the flight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: reducing Docker image size through build optimization.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR rewrites the container build to a multi-stage Dockerfile and trims Python dependencies to significantly reduce the shipped runtime image size while keeping the application runtime behavior the same.

Changes:

  • Replaced the single-stage Docker build with a 3-stage build (builder → model-fetch → runtime) and removed build toolchains from the runtime image.
  • Replaced google-cloud-sdk/gcloud storage cp model fetching with a lightweight Python-based GCS download script.
  • Trimmed dependency extras (pipecat-ai and redis) and regenerated uv.lock; tightened Docker context via .dockerignore.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Dockerfile Multi-stage build; venv + model fetch separated from runtime; runtime now runs python run.py.
scripts/fetch_aic_models.py New GCS model download helper used during build.
pyproject.toml Removes unused extras (pipecat-ai azure/silero; redis[hiredis]).
uv.lock Lockfile regeneration reflecting dependency trimming.
.dockerignore Excludes large/unneeded build context paths; whitelists the new fetch script.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Dockerfile Outdated
VENV_PATH=/opt/venv \
PATH="/opt/venv/bin:${PATH}"

# Runtime-only apt — no compilers, no headers. Skip ffmpeg므 X11/mesa/audio
Comment thread Dockerfile Outdated
Comment on lines +116 to +120
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
--no-install-recommends \
--ignore-missing \
ffmpeg \
libportaudio2 \
Comment thread scripts/fetch_aic_models.py Outdated
Comment on lines +22 to +26
def get_credentials():
if os.path.exists(TOKEN_FILE) and os.path.getsize(TOKEN_FILE) > 0:
token = open(TOKEN_FILE).read().strip()
print("Auth: BuildKit secret gcp_token")
return Credentials(token=token), None

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/fetch_aic_models.py`:
- Line 22: Update the get_credentials function signature with a return type
annotation representing optional Google credentials together with an optional
project ID, using the appropriate typing constructs and existing credential type
imports.
- Around line 64-71: Update the model-fetch loop around the download try/except
to collect each failed model name when credentials are present, while retaining
the existing warning output. After all models are processed, exit non-zero if
any downloads failed; preserve the separate no-credentials AWS exit path.
- Around line 25-32: Replace the authentication diagnostic print calls in the
credential-loading flow with contextual Loguru logger calls. In the exception
handlers around ADC and related credential attempts, use
logger.opt(exception=e).warning(...) without interpolating e into the message;
preserve the existing diagnostic context in each structured message.
- Around line 41-57: Move the GOOGLE_CLOUD_PROJECT/GCP_PROJECT fallback,
AIC_BUCKET_PATH, and AIC_MODELS definitions into app/core/config/static.py,
exposing the required settings through get_required_env(). Update the
model-fetch script to import and use those static configuration values instead
of direct os.environ.get calls, while preserving existing defaults or mandatory
behavior as defined by the configuration. Update the Docker model-fetch stage to
copy the static configuration module before importing it.
- Around line 23-24: Make the script entry point asynchronous and move all
blocking filesystem and Google Cloud operations off the event loop: wrap open,
os.path.exists, os.makedirs, os.path.getsize, blob.download_to_filename, and ADC
lookup in awaitable calls, using asyncio.to_thread for synchronous APIs
including Google Cloud Storage downloads. Update callers to await the async
entry point and preserve the existing token and download behavior.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 70345d55-fe7c-48a3-b1fc-293925f08738

📥 Commits

Reviewing files that changed from the base of the PR and between 8261274 and 44ae98e.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • .dockerignore
  • Dockerfile
  • pyproject.toml
  • scripts/fetch_aic_models.py

Comment thread scripts/fetch_aic_models.py Outdated
DEST_DIR = "/models"


def get_credentials():

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add the required return type annotation.

get_credentials has no type annotation. Add a return type that represents optional Google credentials and an optional project ID.

As per coding guidelines, “Include required type hints on all function signatures.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/fetch_aic_models.py` at line 22, Update the get_credentials function
signature with a return type annotation representing optional Google credentials
together with an optional project ID, using the appropriate typing constructs
and existing credential type imports.

Source: Coding guidelines

Comment thread scripts/fetch_aic_models.py Outdated
Comment on lines +23 to +24
if os.path.exists(TOKEN_FILE) and os.path.getsize(TOKEN_FILE) > 0:
token = open(TOKEN_FILE).read().strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target file outline ---'
ast-grep outline scripts/fetch_aic_models.py || true
printf '%s\n' '--- target file ---'
cat -n scripts/fetch_aic_models.py
printf '%s\n' '--- related configuration and imports ---'
rg -n --glob '*.py' 'fetch_aic_models|get_required_env|TOKEN_FILE|google\.cloud|storage\.Client|download_to_filename|download_as_bytes|open\(' app scripts

Repository: juspay/clairvoyance

Length of output: 5866


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- callers and build context ---'
rg -n --glob '!scripts/fetch_aic_models.py' 'fetch_aic_models\.py|model-fetch|AIC_BUCKET_PATH|AIC_MODELS|/models' .
printf '%s\n' '--- project/runtime configuration ---'
fd -i 'pyproject.toml|Dockerfile*|static.py' . -t f -x sh -c 'echo "--- $1"; sed -n "1,220p" "$1"' sh {}
printf '%s\n' '--- synchronous I/O calls in target file (AST) ---'
python3 - <<'PY'
import ast
from pathlib import Path

path = Path("scripts/fetch_aic_models.py")
tree = ast.parse(path.read_text())
for node in ast.walk(tree):
    if isinstance(node, ast.Call):
        callee = ast.unparse(node.func)
        if any(term in callee for term in (
            "open", "os.path.exists", "os.path.getsize", "os.makedirs",
            "download_to_filename", "storage.Client", "adc_default",
        )):
            print(f"line {node.lineno}: {callee}")
PY

Repository: juspay/clairvoyance

Length of output: 29281


Move blocking I/O behind async calls.

Make the entry point asynchronous. Await filesystem operations and google-cloud-storage downloads. Use asyncio.to_thread for the synchronous client methods until an async GCS client is available. Cover open, os.path.exists, os.makedirs, os.path.getsize, blob.download_to_filename, and the ADC lookup.

🧰 Tools
🪛 ast-grep (0.45.1)

[warning] 23-23: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(TOKEN_FILE)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/fetch_aic_models.py` around lines 23 - 24, Make the script entry
point asynchronous and move all blocking filesystem and Google Cloud operations
off the event loop: wrap open, os.path.exists, os.makedirs, os.path.getsize,
blob.download_to_filename, and ADC lookup in awaitable calls, using
asyncio.to_thread for synchronous APIs including Google Cloud Storage downloads.
Update callers to await the async entry point and preserve the existing token
and download behavior.

Source: Coding guidelines

Comment thread scripts/fetch_aic_models.py Outdated
Comment on lines +25 to +32
print("Auth: BuildKit secret gcp_token")
return Credentials(token=token), None
try:
print("Auth: attempting ADC (metadata server)")
creds, project = adc_default()
return creds, project
except Exception as e:
print(f"Auth: no credentials found ({e})")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use structured Loguru logging for build diagnostics.

Replace print calls with contextual Loguru records. In the exception handlers, use logger.opt(exception=e).warning(...) and do not interpolate e into the message string.

As per coding guidelines, “Use Loguru logging with contextvars for structured logging; send traces to Langfuse for observability.” Based on learnings, exception objects must not be interpolated into Loguru messages.

Also applies to: 69-71

🧰 Tools
🪛 Ruff (0.16.1)

[warning] 31-31: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/fetch_aic_models.py` around lines 25 - 32, Replace the authentication
diagnostic print calls in the credential-loading flow with contextual Loguru
logger calls. In the exception handlers around ADC and related credential
attempts, use logger.opt(exception=e).warning(...) without interpolating e into
the message; preserve the existing diagnostic context in each structured
message.

Sources: Coding guidelines, Learnings

Comment thread scripts/fetch_aic_models.py Outdated
Comment on lines +41 to +57
project = (
os.environ.get("GOOGLE_CLOUD_PROJECT")
or os.environ.get("GCP_PROJECT")
or detected_project
)
client = storage.Client(credentials=creds, project=project)

bucket_path = os.environ.get("AIC_BUCKET_PATH", "gs://breeze-clairvoyance-models/aic")
path = bucket_path.replace("gs://", "", 1)
parts = path.split("/", 1)
bucket_name = parts[0]
prefix = parts[1] if len(parts) > 1 else ""

models = os.environ.get(
"AIC_MODELS",
"quail_l_8khz.aicmodel quail_l_16khz.aicmodel quail_vf_2_1_l_16khz.aicmodel",
).split()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Move model-fetch configuration into static configuration.

These direct os.environ.get calls bypass app/core/config/static.py. Define the project, bucket, and model-list settings in static configuration, then load mandatory settings with get_required_env().

The Docker model-fetch stage must copy the required static configuration module before this script imports it.

As per coding guidelines, “Load ALL configuration from app/core/config/static.py using get_required_env() for mandatory variables; never import directly from os.environ elsewhere.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/fetch_aic_models.py` around lines 41 - 57, Move the
GOOGLE_CLOUD_PROJECT/GCP_PROJECT fallback, AIC_BUCKET_PATH, and AIC_MODELS
definitions into app/core/config/static.py, exposing the required settings
through get_required_env(). Update the model-fetch script to import and use
those static configuration values instead of direct os.environ.get calls, while
preserving existing defaults or mandatory behavior as defined by the
configuration. Update the Docker model-fetch stage to copy the static
configuration module before importing it.

Source: Coding guidelines

Comment thread scripts/fetch_aic_models.py Outdated
Comment on lines +64 to +71
try:
bucket = client.bucket(bucket_name)
blob = bucket.blob(blob_path)
blob.download_to_filename(dest)
size = os.path.getsize(dest)
print(f"Fetched {model_name}: {size} bytes")
except Exception as e:
print(f"Warning: failed to fetch {model_name}: {e}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail the build when authenticated model downloads fail.

Line 70 catches every download error and allows the script to exit successfully. The runtime code then detects the missing artifact and disables the AIC filter. A GCP build with invalid permissions, an invalid bucket, or a missing model can therefore publish an image without the configured noise filter.

Keep the no-credentials AWS exit path. If credentials exist, collect failed model names and exit non-zero after the loop.

🧰 Tools
🪛 Ruff (0.16.1)

[warning] 70-70: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/fetch_aic_models.py` around lines 64 - 71, Update the model-fetch
loop around the download try/except to collect each failed model name when
credentials are present, while retaining the existing warning output. After all
models are processed, exit non-zero if any downloads failed; preserve the
separate no-credentials AWS exit path.

- Replace google-cloud-sdk install (~845MB unpacked) with plain curl +
  Bearer token against GCS JSON API (same BuildKit gcp_token contract,
  curl already installed in image's apt step)
- useradd and 'COPY --chown=appuser:appuser' in one step instead of
  'chown -R /app' in a later layer (eliminates 1.82GB duplicate layer)
- Delete uv cache inside the same layer that fills it (rm -rf /app/.uv-cache
  after both 'uv sync' and 'nltk.downloader' steps) because layers are
  additive
- 'uv run --no-sync' as CMD: keeps the already-built venv immutable and
  lets us skip a 'chown -R /app/.venv' without giving up dependency
  determinism

Verified on Cloud Build E2_HIGHCPU_8 + GKE registry:
- image 1,705 MB → 857 MB (-50%)
- build time unchanged (~5 min)
- AIC models downloaded into /app/models/voice/aic (89MB)
- /health returns 200; pipecat services import cleanly; subprocess-spawned
  voice bot (daily.py pattern) boots

Co-authored-by: harsh.tiwari <harsh.tiwari@juspay.in>
@cmd-err
cmd-err force-pushed the feat/docker-image-optimization branch from 44ae98e to cb49323 Compare August 11, 2026 11:17
@cmd-err cmd-err changed the title perf(build): slim Docker image 1.63GB → 0.58GB (-66%) perf(build): slim Docker image 1.7GB → 0.85GB (-50%) Aug 11, 2026
@cmd-err cmd-err changed the title perf(build): slim Docker image 1.7GB → 0.85GB (-50%) perf(build): slim Docker image 1.63GB → 0.81GB (-50.31%) Aug 11, 2026
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.

2 participants