feat(docker): official Vite+ toolchain image#1944
Conversation
✅ Deploy Preview for viteplus-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
✅ Staging deployment successful! Preview: https://viteplus-staging.void.app/ |
vite-plus
@voidzero-dev/vite-plus-core
@voidzero-dev/vite-plus-prompts
@voidzero-dev/vite-plus-cli-darwin-arm64
@voidzero-dev/vite-plus-cli-darwin-x64
@voidzero-dev/vite-plus-cli-linux-arm64-gnu
@voidzero-dev/vite-plus-cli-linux-arm64-musl
@voidzero-dev/vite-plus-cli-linux-x64-gnu
@voidzero-dev/vite-plus-cli-linux-x64-musl
@voidzero-dev/vite-plus-cli-win32-arm64-msvc
@voidzero-dev/vite-plus-cli-win32-x64-msvc
@voidzero-dev/vite-plus-darwin-arm64
@voidzero-dev/vite-plus-darwin-x64
@voidzero-dev/vite-plus-linux-arm64-gnu
@voidzero-dev/vite-plus-linux-arm64-musl
@voidzero-dev/vite-plus-linux-x64-gnu
@voidzero-dev/vite-plus-linux-x64-musl
@voidzero-dev/vite-plus-win32-arm64-msvc
@voidzero-dev/vite-plus-win32-x64-msvc
commit: |
Manual verification commands for the Docker preview imagesPreview images on GHCR (from the
1. Basic functionalitydocker run --rm ghcr.io/voidzero-dev/vite-plus:pr-1944 vp --version
docker run --rm ghcr.io/voidzero-dev/vite-plus:pr-1944 sh -c \
'id -un; command -v vp; vp help >/dev/null && echo "vp help ok"; git --version'2. Common
|
Propose an official Vite+ toolchain Docker image on GHCR that bundles the vp CLI for the build/CI/dev phases, plus a documented multi-stage pattern that copies the exact .node-version Node into a slim glibc runtime (no vp), keeping deployed images small while honoring the project's pinned Node. Refs #1490, #1324
The image installs vp from npm via the official install script (pinned VP_VERSION) and publishes after the npm release, rather than copying release artifacts. Mark the RFC accepted with implementation in progress.
Add docker/Dockerfile for the official Vite+ toolchain image: a glibc (debian:bookworm-slim) image that bundles the vp CLI for the build, CI, and development phases. vp provisions the exact Node.js from .node-version at build time, so the image is version-agnostic and needs no Node-keyed tags. Add a publish-docker job to release.yml that builds the multi-arch (amd64/arm64) image and pushes it to ghcr.io/voidzero-dev/vite-plus, tagged by vp version, after the npm release is published. Add docs/guide/docker.md documenting the recommended multi-stage pattern that copies the resolved Node.js into a small, vp-free production runtime image, plus static-SPA, CI, devcontainer, and ad-hoc usage. Refs #1490
Add a publish-docker-preview job to publish-to-pkg.pr.new.yml that builds the multi-arch image from the PR's pkg.pr.new build (VP_PR_VERSION) and pushes it as ghcr.io/voidzero-dev/vite-plus:pr-<number>, so the image can be verified before a real release. Teach docker/Dockerfile an optional VP_PR_VERSION build arg, which installs vp from pkg.pr.new instead of npm. Refs #1490
Fixes vp check formatting failures in docs/guide/docker.md and rfcs/docker-image.md (table alignment and emphasis markers).
- Drop xz-utils: vp only extracts .tar.gz (gzip), never xz. - Drop redundant `mkdir -p /app && chown`: WORKDIR /app under USER vp already creates it owned by vp (verified). - Combine the two ENV instructions into one layer. - Build the per-PR preview image for linux/amd64 only; arm64 is covered by the release build and the test-install-sh-arm64 job, avoiding the slow QEMU leg on every labeled PR.
Reference why-reproductions-are-required/vite-plus-docker-example, which CI-verifies the documented Dockerfile patterns end to end.
Running vp install --prod after a full vp install does not prune the already-installed devDependencies (the large vite-plus toolchain), so the docs pattern shipped ~164MB of dev toolchain into the runtime via COPY node_modules. Install production dependencies in a dedicated deps stage (fresh --prod) instead, and note that self-contained bundles can skip node_modules entirely. Also fix the runtime size claim (smaller than the default node:* image, not -slim).
The installer pre-provisions a default Node.js (~190MB), but each project provisions its own pinned Node at build time, so the default is dead weight in a builder image. Remove it (rm -rf $VP_HOME/js_runtime) in the install layer; the node/npm/npx shims remain and fetch the right version on first use. Toolchain image: ~1.04GB -> ~846MB, more than an Alpine switch would save and without the musl tradeoffs.
Publish an opt-in Alpine variant under -alpine tags (docker/Dockerfile.alpine), built via a debian+alpine matrix in both the release and pkg.pr.new preview workflows. It yields the smallest runtime (Alpine SSR ~136MB vs ~150MB distroless, ~198MB debian-slim) for teams that standardize on Alpine. Document the musl tradeoffs loudly: Node comes from the unofficial, unsigned musl builds; native addons may need musl prebuilds or source compilation; and a musl Node binary only runs on a musl base, so the runtime stage must also be Alpine. The Debian image stays the recommended default.
Switch the example tags from the fictional :1 to the real 0.x scheme (:0, :0.2, :0.2.2 and -alpine variants), since 0.2.2 is the first published image. Add a link to the GitHub package page to browse all published versions and digests.
The image runs as the non-root vp user, so COPY without --chown writes root-owned files that vp install cannot update (permission denied) when it needs to write package.json or the lockfile (e.g. no committed lockfile, or vp add). Use COPY --chown=vp:vp in the build/deps stages. Verified end to end against the published pr-1944 preview image.
After both preview variants publish, post (or update) a single PR comment with the pr-<n> / pr-<n>-alpine docker pull commands. Uses a hidden marker so re-runs reuse the same comment instead of creating new ones. Implemented with the existing actions/github-script (no new dependency); needs pull-requests: write.
Document why the sticky-comment body uses a line array (YAML block-scalar vs fenced code blocks) and add 'keep in sync' notes on the install RUN line shared verbatim between docker/Dockerfile and docker/Dockerfile.alpine.
Measure each published preview image (docker pull + image inspect) and render a size table in the comment, alongside the pull commands. Addresses review feedback on #1944.
🐳 Docker preview imagesBuilt from this PR's pkg.pr.new build:
docker pull ghcr.io/voidzero-dev/vite-plus:pr-1944
docker pull ghcr.io/voidzero-dev/vite-plus:pr-1944-alpineQuick check: docker run --rm ghcr.io/voidzero-dev/vite-plus:pr-1944 vp --versionSee docs/guide/docker.md for usage. |
|
An exception was found that may be related to the vite task. The lint command runs normally when executed directly, but throws an error after passing through the task runner. docker run --rm ghcr.io/voidzero-dev/vite-plus:pr-1944 bash -c \
'vp create vite:monorepo --directory hello --no-interactive && cd hello && vp lint && vp run ready'
◇ Scaffolded hello with Vite+ monorepo
• Node 24.18.0 pnpm 11.9.0
✓ Dependencies installed in 27s
→ Next: cd hello && vp run
Found 0 warnings and 0 errors.
Finished in 1.2s on 6 files with 111 rules using 16 threads.
$ vp check
pass: All 18 files are correctly formatted (1112ms, 16 threads)
Error running tsgolint: "exit status: exit status: 1"/app/hello/node_modules/.pnpm/oxlint-tsgolint@0.23.0/node_modules/oxlint-tsgolint/bin/tsgolint.js:18
throw e;
^
<ref *1> Error: spawnSync /app/hello/node_modules/.pnpm/@oxlint-tsgolint+linux-x64@0.23.0/node_modules/@oxlint-tsgolint/linux-x64/tsgolint EINVAL
at Object.spawnSync (node:internal/child_process:1143:20)
at spawnSync (node:child_process:911:24)
at Object.execFileSync (node:child_process:954:15)
at Object.<anonymous> (/app/hello/node_modules/.pnpm/oxlint-tsgolint@0.23.0/node_modules/oxlint-tsgolint/bin/tsgolint.js:11:17)
at Module._compile (node:internal/modules/cjs/loader:1871:14)
at Object..js (node:internal/modules/cjs/loader:2002:10)
at Module.load (node:internal/modules/cjs/loader:1594:32)
at Module._load (node:internal/modules/cjs/loader:1396:12)
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5) {
errno: -22,
code: 'EINVAL',
syscall: 'spawnSync /app/hello/node_modules/.pnpm/@oxlint-tsgolint+linux-x64@0.23.0/node_modules/@oxlint-tsgolint/linux-x64/tsgolint',
path: '/app/hello/node_modules/.pnpm/@oxlint-tsgolint+linux-x64@0.23.0/node_modules/@oxlint-tsgolint/linux-x64/tsgolint',
spawnargs: [ 'headless' ],
error: [Circular *1],
status: null,
signal: null,
output: null,
pid: 0,
stdout: undefined,
stderr: undefined
}
Node.js v24.18.0
Linting failed before analysis started
error: Linting could not startcc @wan9chi |
Avoid hardcoded version tags in the runnable examples so users do not copy an outdated pin; the tags table now documents the scheme with placeholders.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d36ba17fad
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| WORKDIR /app | ||
|
|
||
| # Install dependencies first so this layer is cached across source changes. | ||
| COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./ |
There was a problem hiding this comment.
Do not require
.node-version in the Docker examples
This example makes .node-version a mandatory Docker COPY source even though the guide says projects can pin Node via engines.node or devEngines.runtime, and the resolver does support those fallbacks. For projects that rely only on package.json, Docker fails at this COPY step before vp can read the package metadata; the same required source is repeated in the deps/static/Alpine examples. Please either make the baseline examples copy only files that always exist or show .node-version as an optional variant.
Useful? React with 👍 / 👎.
| && vp --version \ | ||
| && rm -rf "$VP_HOME/js_runtime" | ||
|
|
||
| WORKDIR /app |
There was a problem hiding this comment.
Create
/app with ownership for the vp user
When downstream Dockerfiles follow the new guide and run WORKDIR /app plus RUN vp install as the default vp user, the inherited /app directory from this image is root-owned because Docker creates missing WORKDIR directories as root even after USER. COPY --chown=vp:vp ... fixes the files but not the directory itself, so vp install cannot create node_modules under /app; the Alpine image has the same pattern. Please create/chown /app before switching users or set the workdir somewhere already writable by vp.
Useful? React with 👍 / 👎.
Implements the official Vite+ Docker image from the RFC (
rfcs/docker-image.md).vpalready provisions the exact Node.js from.node-version, so one toolchain image builds any project (no Node-version-keyed tags). It is not a production runtime image: a documented multi-stage build copies the resolved Node.js into a small, vp-free runtime stage.Changes
docker/Dockerfile: glibc (debian:bookworm-slim) image bundlingvp+ native build toolchain, non-root user.release.yml:publish-dockerjob, multi-arch (amd64/arm64) push toghcr.io/voidzero-dev/vite-plusafter npm publish, version-tagged.docs/guide/docker.md+ sidebar: multi-stage runtime, static SPA, CI, devcontainer, ad-hoc usage.Verified locally:
.node-version=24.15.0resolves and installs exactly v24.15.0; the copied Node runs standalone in a plaindebian:bookworm-slimstage.Note: the first publish needs the GHCR package made public / linked to the repo in org settings.
Closes #1490