Skip to content

docs: ship deno deps to runtime stage in multi-stage Docker example#3120

Closed
lunadogbot wants to merge 2 commits into
mainfrom
orch/issue-65
Closed

docs: ship deno deps to runtime stage in multi-stage Docker example#3120
lunadogbot wants to merge 2 commits into
mainfrom
orch/issue-65

Conversation

@lunadogbot
Copy link
Copy Markdown
Contributor

Summary

The multi-stage Dockerfile example on /runtime/reference/docker/ runs
deno install --entrypoint main.ts in the build stage, but deno install
populates Deno's global cache ($DENO_DIR), not the project directory.
The runtime stage only does COPY --from=builder /app ., so the cached
deps never reach it and the container re-downloads them on first run
(which also breaks any deployment that runs with --network none or
without outbound internet access).

This PR points $DENO_DIR at /deno-dir in both stages and adds a
second COPY --from=builder /deno-dir /deno-dir so the populated cache
ships with the image. A short paragraph below the snippet explains the
why.

The single-stage snippet at the top of the page wasn't affected — it
never moves the app between stages, so the existing deno install
already lives next to the runtime.

Why this approach over --vendor

The reporter suggested deno install --vendor. I tried it and confirmed
that the --vendor flag alone is not sufficient: it does write deps
into /app/vendor/, but Deno only uses that folder at runtime when
the project has "vendor": true in its deno.json (or an equivalent
config). For a generic example that may not have a deno.json yet, the
$DENO_DIR route is more reliable — it doesn't depend on the user
adding any config and works for both jsr/https/npm specifiers
transparently.

Verification

Built the updated Dockerfile against denoland/deno:latest with a tiny
main.ts that imports a remote module from JSR, then ran the resulting
image with docker run --network none …:

main.ts:

import { delay } from "jsr:@std/async@^1.0.0/delay";

console.log("Starting...");
await delay(100);
console.log("Hello from a deps-shipped container!");

Build output (excerpt):

#8 [builder 4/4] RUN deno install --entrypoint main.ts
#8 0.124 Download https://jsr.io/@std/async/meta.json
#8 0.337 Download https://jsr.io/@std/async/1.3.0_meta.json
#8 0.494 Download https://jsr.io/@std/async/1.3.0/delay.ts
#8 0.655 Installed 1 package in 539ms
#8 0.655 + jsr:@std/async 1.3.0
#8 DONE 0.7s

#9 [stage-1 3/4] COPY --from=builder /app .
#10 [stage-1 4/4] COPY --from=builder /deno-dir /deno-dir

Offline run:

$ docker run --rm --network none deno-fix-final
Starting...
Hello from a deps-shipped container!

For comparison, the same main.ts against the previous (unfixed) snippet
fails offline:

$ docker run --rm --network none deno-broken
Download https://jsr.io/@std/async/meta.json
error: JSR package manifest for '@std/async' failed to load. Import 'https://jsr.io/@std/async/meta.json' failed.
    at file:///app/main.ts:1:23

Test plan

  • Reproduced the bug with the original Dockerfile (--network none → re-download → fail)
  • Built the updated Dockerfile against denoland/deno:latest (Deno 2.7.14)
  • Ran the resulting image with --network none and confirmed the app starts and serves without re-fetching

Closes #2935
Closes bartlomieju/orchid-inbox#65

The multi-stage Dockerfile example ran `deno install --entrypoint main.ts`
in the build stage, but `deno install` populates Deno's global cache
($DENO_DIR), not the project directory. With only `COPY --from=builder
/app .` in the runtime stage, the cached deps never made it across and
the container re-downloaded them on first run.

Point $DENO_DIR at /deno-dir in both stages and add a second COPY so the
populated cache rides along with the app. Verified by building against
denoland/deno:latest and running with `docker run --network none …`:
the container now starts and prints output without re-fetching.

Closes #2935
Copy link
Copy Markdown
Contributor

@fibibot fibibot left a comment

Choose a reason for hiding this comment

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

Two things to flag before this merges:

  1. PR #3030 (already APPROVED, same Closes #2935) is the same fix from another angle — copies /deno-dir from the builder and adds the explanatory paragraph. It also rearranges the single-stage example to cache deps as a separate layer, which this PR doesn't do. Likely a coordination/duplicate situation worth resolving before either lands.
  2. The new ENV DENO_DIR=/deno-dir lines in both stages are redundant — the official denoland/deno image already sets DENO_DIR=/deno-dir/, which is what makes the existing "Environment Variables" section at line 124 of this same file work. The COPY --from=builder /deno-dir /deno-dir alone is sufficient. Not wrong, just extra.
  • nit: substantive content change but last_modified: 2025-12-16 isn't bumped (style guide says it should be).
  • nit: /deno-dir (no trailing slash) vs the existing /deno-dir/ on line 124 — pick one for consistency.

Holding any verdict until lint and link check is green. The current failure is the same lume/jsx-runtime build error affecting other open PRs, not anything in this diff.

@lunadogbot
Copy link
Copy Markdown
Contributor Author

Closing in favor of #3030, which:

  • already has the same COPY --from=builder /deno-dir /deno-dir fix and an APPROVED review,
  • comes from the maintainer named in the upstream issue,
  • additionally reorganizes the basic example to put deno install in its own cached layer (which this PR does not do),
  • avoids the redundant ENV DENO_DIR=/deno-dir lines this PR added (the official denoland/deno image already sets that, as @fibibot correctly flagged).

Deferring to #3030 to avoid duplicate work. Thanks @fibibot for catching the overlap.

@lunadogbot lunadogbot closed this May 14, 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.

Feedback: /runtime/reference/docker/ - Needs Improvement

2 participants