Skip to content

fix(docker): bake prisma offline in the componentized migrations image - #35485

Merged
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_migrations_offline_prisma_bake
Aug 1, 2026
Merged

fix(docker): bake prisma offline in the componentized migrations image#35485
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_migrations_offline_prisma_bake

Conversation

@yassin-berriai

@yassin-berriai yassin-berriai commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Componentized migrations image downloads Node at startup
  • prisma migrate deploy hangs in egress-restricted clusters
  • Prisma bake sits behind a uid-specific $HOME path
  • Root/non_root/database images were fixed; this one was missed

How it solves it:

  • Bake prisma CLI and engines at /opt/prisma, world-readable
  • Install nodejs in the migrations runtime stage
  • Pin PRISMA_CLI_PATH / PRISMA_OFFLINE_MODE so no download is attempted
  • Cover the image in the existing offline-migration CI check

Relevant issues

Same failure class as #33167 and #24554, which were fixed for the root, Dockerfile.database and Dockerfile.non_root images. The componentized migrations/Dockerfile was never brought in line; this does that

Linear ticket

Resolves LIT-4727

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

The end-user surface here is the container, so the proof is the image itself running the way an air-gapped cluster runs it: a --internal docker network (no egress at all) with a reachable Postgres on it. Before captured at base 0a42f28850, after at d33e75853a, then re-run at 355aaf50c1 after a comment-only amend

Before, base image, no egress, arbitrary non-root uid

$ docker build -f migrations/Dockerfile -t litfix-4727/migrations:base .
$ docker network create --internal litfix-4727-airgap
$ docker run -d --name litfix-4727-pg3 --network litfix-4727-airgap \
    -e POSTGRES_PASSWORD=litellm -e POSTGRES_USER=litellm -e POSTGRES_DB=litellm postgres:16
$ docker run --rm --network litfix-4727-airgap --user 12345:0 \
    -e DATABASE_URL="postgresql://litellm:litellm@litfix-4727-pg3:5432/litellm" \
    litfix-4727/migrations:base
...
subprocess.CalledProcessError: Command '['prisma', 'migrate', 'deploy']' returned non-zero exit status 1.
exit=1

$ docker exec litfix-4727-pg3 psql -U litellm -d litellm -tAc \
    "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';"
0

Run as the image's own default uid, the same base image shows the download attempt directly:

  File "/app/.venv/lib/python3.13/site-packages/prisma/cli/_node.py", line 161, in resolve
    return NodeBinaryStrategy.from_nodeenv(target)
  File "/app/.venv/lib/python3.13/site-packages/prisma/cli/_node.py", line 174, in from_nodeenv
    subprocess.run(
urllib.error.URLError: <urlopen error [Errno -3] Temporary failure in name resolution>
nodeenv installation failed; You may want to try installing `nodejs-bin` as it is more reliable.
subprocess.CalledProcessError: Command '['/app/.venv/bin/python', '-m', 'nodeenv', '/home/nonroot/.cache/prisma-python/nodeenv']' returned non-zero exit status 1.

After, same network, same flags

$ docker build -f migrations/Dockerfile -t litfix-4727/migrations:fixed .
$ docker run --rm --network litfix-4727-airgap --user 12345:0 \
    -e DATABASE_URL="postgresql://litellm:litellm@litfix-4727-pg2:5432/litellm" \
    litfix-4727/migrations:fixed
...
All migrations have been successfully applied.
2026-08-01 19:42:49,244 - litellm_proxy_extras - INFO - Migration job completed successfully.
exit=0

$ docker exec litfix-4727-pg2 psql -U litellm -d litellm -tAc \
    "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';"
69

After, default uid, read-only root filesystem

This is the constraint the report came in under, so it gets its own run

$ docker run --rm --network litfix-4727-airgap --read-only \
    -e DATABASE_URL="postgresql://litellm:litellm@litfix-4727-pg4:5432/litellm" \
    litfix-4727/migrations:fixed
...
All migrations have been successfully applied.
2026-08-01 19:44:50,040 - litellm_proxy_extras - INFO - Migration job completed successfully.
exit=0
69

The regression test discriminates

tests/proxy_migration_tests/test_offline_image_migration.py already encodes this contract for the single image; the change makes the migration invocation configurable so the same suite covers this image, and a new migrations-image job in image-scan.yml runs it

$ export LITELLM_MIGRATION_INTERPRETER=python3 LITELLM_MIGRATION_SCRIPT=/app/run.py

$ LITELLM_IMAGE=litfix-4727/migrations:fixed pytest tests/proxy_migration_tests/test_offline_image_migration.py -q
..                                                                       [100%]
2 passed in 12.00s

$ LITELLM_IMAGE=litfix-4727/migrations:base pytest tests/proxy_migration_tests/test_offline_image_migration.py -q
E   PermissionError: [Errno 13] Permission denied: '/home/nonroot/.cache/prisma-python/binaries/5.4.2/ac9d7041ed77bcc8a8dbd2ab6616b39013829574'
E   assert 1 == 0
FAILED tests/proxy_migration_tests/test_offline_image_migration.py::test_migration_offline_as_non_root_uid
1 failed, 1 passed in 10.14s

Re-run after the comment-only amend, rebuilding the image from 355aaf50c1, to confirm the discriminator survived it

### FIXED @ 355aaf50c1 (expect PASS)
2 passed in 6.33s
### BASE @ 0a42f28850 (expect FAIL)
FAILED tests/proxy_migration_tests/test_offline_image_migration.py::test_migration_offline_as_non_root_uid
1 failed, 1 passed in 10.20s

Type

🐛 Bug Fix

Changes

migrations/Dockerfile now bakes the prisma CLI and engines under /opt/prisma with a+rX modes instead of anchoring them in $HOME, installs nodejs in the runtime stage, and pins PRISMA_BINARY_CACHE_DIR, PRISMA_CLI_PATH, PRISMA_CLI_QUERY_ENGINE_TYPE and PRISMA_OFFLINE_MODE. Those last two env vars are what litellm_proxy_extras.utils._get_prisma_command and _get_prisma_env read to run the cached CLI directly rather than falling through the python wrapper, which is the path that reaches for nodeenv. The build now asserts the two baked artifacts exist, so a future edit that breaks the bake fails the build instead of the cluster

There are two distinct symptoms behind one root cause. Without node in the runtime stage prisma-client-py resolves its node target through nodeenv and downloads a runtime; with the bake behind /home/nonroot, an arbitrary uid cannot read it. Moving to /opt/prisma plus a runtime node closes both, which is exactly what the root, non_root and database images already do

Worth calling out why this survived the earlier round of fixes: grep -rn 'migrations/Dockerfile\|gateway/Dockerfile\|backend/Dockerfile' .github/ ci_cd/ Makefile .circleci/config.yml came back empty on the base branch, so no job in this repo built any componentized image and nothing would have caught it. The migrations-image job here is the first

No grype step was added for this image. Its runtime apk set is a subset of Dockerfile.non_root, which the existing scan job covers, and nodejs was already in that image's runtime layer, so this introduces no unscanned package surface

Scope note: the gateway and backend images also lack node in their runtime stage, but neither runs a prisma schema update. That path lives in proxy_cli.py, which the componentized entrypoints bypass by importing the ASGI app directly, so they are unaffected

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

@yassin-berriai
yassin-berriai requested a review from a team August 1, 2026 19:47
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR makes the standalone migrations image self-contained for offline execution and adds image-level CI coverage

  • Bakes the Prisma CLI and engines into a world-readable /opt/prisma location
  • Adds the runtime Node.js dependency and configures Prisma to use the baked artifacts
  • Extends the offline migration test harness and runs it against the componentized migrations image

Confidence Score: 5/5

The PR appears safe to merge

No blocking failure remains

Important Files Changed

Filename Overview
migrations/Dockerfile Moves baked Prisma artifacts to /opt/prisma, adds runtime Node.js, and configures offline migration execution; the previously reported comment issue is no longer present
.github/workflows/image-scan.yml Adds a dedicated job that builds and exercises the migrations image in the existing offline migration test
tests/proxy_migration_tests/test_offline_image_migration.py Makes the migration command configurable so the same image-level regression test covers both runtime image layouts

Reviews (2): Last reviewed commit: "fix(docker): bake prisma offline in the ..." | Re-trigger Greptile

Comment thread migrations/Dockerfile Outdated
Comment on lines +71 to +80

# Pre-warm the Prisma binary cache so the Job pod doesn't reach the
# internet on first start. This matches what the backend Dockerfile does:
# `prisma generate` runs nodeenv (downloads Node), installs the prisma npm
# CLI, downloads the engine binaries for each `binaryTarget` in
# schema.prisma, AND emits the generated Python client. We don't need the
# client at runtime — the migration job invokes `prisma migrate deploy`
# via subprocess — but having it cached is harmless and the alternative
# (`prisma py fetch`) doesn't reliably trigger engine downloads.
RUN mkdir -p /home/nonroot && \
HOME=/home/nonroot prisma generate --schema=./schema.prisma && \
chown -R nonroot:nonroot /home/nonroot/.cache
# Bake the Prisma CLI and engines so the Job pod doesn't reach the internet
# on first start. `prisma generate` installs the prisma npm CLI, downloads
# the engine binaries for each `binaryTarget` in schema.prisma, AND emits the
# generated Python client. We don't need the client at runtime — the
# migration job invokes `prisma migrate deploy` via subprocess — but having
# it cached is harmless and the alternative (`prisma py fetch`) doesn't
# reliably trigger engine downloads.
#
# The bake lands under /opt/prisma, a fixed path every runtime uid can read

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 New comments violate repository guidance

The newly added explanatory prose conflicts with the repository rule prohibiting new code comments unless explicitly requested, and it adds documentation that must remain synchronized across the Dockerfile, workflow, and test harness.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

The migrations image ran `prisma migrate deploy` against a bake anchored in
$HOME with no node in the runtime stage, so prisma-client-py fell through to
nodeenv and tried to download a Node runtime on first start. In an
egress-restricted cluster that fails outright, and under an arbitrary uid the
uid-specific cache path is unreadable, so the job never applies a migration.

Move the bake to /opt/prisma with world-readable modes, install node in the
runtime stage, and pin PRISMA_BINARY_CACHE_DIR / PRISMA_CLI_PATH /
PRISMA_OFFLINE_MODE so the migration entrypoint runs the cached CLI directly.
This is the same treatment the root, non_root and database images already
carry.

Resolves LIT-4727
@yassin-berriai
yassin-berriai force-pushed the litellm_migrations_offline_prisma_bake branch from d33e758 to 355aaf5 Compare August 1, 2026 19:56
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head 355aaf50c1

You were right about the comments; the repository guide forbids adding new ones and I had added three. All of them are gone, and the diff against the base now contains zero added comment lines (git diff 0a42f28850 -- <the three files> | grep '^+\s*#' is empty). The block above the prisma generate step is the pre-existing comment restored verbatim, not a rewrite.

Nothing else changed, so I rebuilt the image from the amended commit and re-ran the regression to confirm the discriminator survived: it passes on the new image and still fails on the base image, which is the behaviour the new migrations-image CI job now enforces.

On the concern the comments were carrying: the guard is the test, not the prose. If a future edit drops nodejs from the runtime stage or moves the bake back under $HOME, test_migration_offline_as_non_root_uid fails in CI rather than the cluster.

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@yassin-berriai
yassin-berriai enabled auto-merge (squash) August 1, 2026 21:10
@yassin-berriai
yassin-berriai merged commit 72c8888 into litellm_internal_staging Aug 1, 2026
79 checks passed
@yassin-berriai
yassin-berriai deleted the litellm_migrations_offline_prisma_bake branch August 1, 2026 21:12
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.

4 participants