From 35b55a63aa43a50f2f80ec08a352b6bfddd33dc8 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 13:42:20 +0800 Subject: [PATCH 01/18] docs(rfc): add official Docker image RFC 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 --- rfcs/docker-image.md | 407 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 rfcs/docker-image.md diff --git a/rfcs/docker-image.md b/rfcs/docker-image.md new file mode 100644 index 0000000000..0517f2eb45 --- /dev/null +++ b/rfcs/docker-image.md @@ -0,0 +1,407 @@ +# RFC: Official Vite+ Docker Image + +- Issue: [#1490](https://github.com/voidzero-dev/vite-plus/issues/1490) +- Plan: [#1324](https://github.com/voidzero-dev/vite-plus/issues/1324) ("Distribute `vp` across Homebrew, Windows installer, Docker image, apt etc.") +- Status: Draft + +## Summary + +Publish an official Vite+ Docker image to GHCR that bundles the `vp` global CLI +for the **build, CI, and development** phases. The image is a toolchain image, +not a production runtime image. Because `vp` already reads `.node-version` / +`engines.node` / `devEngines.runtime` and downloads that exact Node version, the +image needs no Node-version-specific tags: one image builds any project against +its pinned Node. + +For production, this RFC does not ship a runtime image. Instead it documents a +multi-stage pattern where the `vp` builder resolves and downloads the exact +official (glibc, signature-verified) Node, and a slim final stage copies just +that Node binary plus the built app and production dependencies into a small +glibc base (no `vp`). This keeps deployed images small while honoring the +project's pinned Node version, which is what [#1490](https://github.com/voidzero-dev/vite-plus/issues/1490) +asks for. + +## Motivation + +### The problem (#1490) + +When containerizing a Vite+ project, users need the Node version to match the +project's `.node-version` exactly. The reporter's project pins `24.15.0`: + +```text +Environment: + Package manager pnpm v10.33.2 + Node.js v24.15.0 (.node-version) +``` + +Their options today both have downsides: + +- `node:24-alpine` matches the major version and is reasonably small, but it is + musl-based and roughly doubles the image size in their case, and the tag does + not pin the exact patch version. +- `alpine:3.23` + `apk add nodejs` is much smaller, but Alpine currently ships + `24.14.1`, which does not match the pinned `24.15.0`. + +There is no Vite+ Docker image or documented Docker pattern that keeps the +container Node aligned with `.node-version`. This RFC provides both. + +### Why Vite+ is well positioned + +Every comparable tool delegates the Node version to the base `node:*` image tag +and only manages the *package manager* (via Corepack). Vite+ already manages the +Node runtime itself: it reads the project's config and downloads the exact Node, +verifying the official `SHASUMS256.txt.asc` PGP signature (see +[`js-runtime.md`](./js-runtime.md) and +[`verify-node-shasums-signature.md`](./verify-node-shasums-signature.md)). The +Docker story can build directly on that machinery instead of reinventing +version pinning with image tags. + +## Prior art + +Researched against current official docs (2026-06-25). Summary of how +comparable tools handle Node version + Docker: + +| Tool | Official image | How the Node version is set | Default base | musl/Alpine stance | +| ------------------- | ----------------------- | ----------------------------------------------- | ------------------- | ------------------------------------------- | +| Volta | no (community only) | `volta` field in package.json, auto-fetch | glibc only | unsupported (libc dependency) | +| mise | exists but "do not use" | `mise install` from `.tool-versions`/`mise.toml`| debian-slim | discouraged; needs `MISE_LIBC=musl` | +| proto / moon | no (moon docs only) | layered on top of `node:*` base | `node:latest` | needs `MOON_TOOLCHAIN_FORCE_GLOBALS=true` | +| asdf | no (community only) | `asdf install` from `.tool-versions` | community | per-plugin; glibc Node by default | +| pnpm | yes (`ghcr.io/pnpm/pnpm`, no Node) | base `node:*` tag + Corepack | debian-slim | not addressed | +| Yarn | no | base `node:*` tag + Corepack (`packageManager`) | `node:*` | n/a | +| Turborepo | no | base `node:*` tag; `turbo prune --docker` | `node:*-alpine` | adds `libc6-compat` | +| Nx | no | base `node:*` tag; `prune-lockfile` | `node:lts-alpine` | not addressed | +| Bun | yes (`oven/bun`) | own runtime | debian; offers distroless | not discussed | +| Deno | yes (Hub + GHCR) | own runtime; ships a `:bin` image to copy in | debian; offers distroless | non-root default | +| Node official | yes | the tag is the version | debian (`-slim`, `-alpine`) | warns musl breaks glibc apps | +| distroless nodejs | yes (`gcr.io/distroless/nodejsNN`) | copy artifacts in | debian/glibc, ~45MB | glibc only | + +Key takeaways that shape this RFC: + +1. **No one else manages Node from a config file in a usable published image.** + The version managers (Volta, mise, proto, asdf) either ship no official image + or one flagged unusable, and they all hit the musl wall because managed Node + means official glibc builds. The package-manager and monorepo tools pin Node + only via the base `node:*` tag. Vite+ collapsing both axes (Node + toolchain) + into one deterministic, project-driven build step is a genuine differentiator. + +2. **The closest analog (mise) and the runtimes (Deno) validate the chosen + pattern.** mise's documented best practice is to copy the small static binary + into a slim glibc base and install the pinned tool at build time, not to ship + a fat all-in-one image. Deno ships a `:bin` image precisely so users can + `COPY --from=denoland/deno:bin /deno ...` into any base, and its distroless + variant copies just the binary onto `gcr.io/distroless/cc`. This is exactly + the multi-stage "copy the resolved Node in" pattern below. + +3. **glibc is the consensus default.** Every Node-managing tool warns about or + breaks on musl. Defaulting to glibc keeps official signature-verified Node + (the unofficial musl builds publish no PGP signature) and avoids native-addon + surprises. + +4. **Monorepo pruning is the one capability plain package managers lack.** + Turborepo `turbo prune --docker` and Nx `prune-lockfile` exist only because a + shared lockfile makes one package's change rebuild every container. Vite+ + owns the workspace graph, so a future `vp prune --docker` is a natural + follow-up (see Future Work). + +Sources: pnpm ; Turborepo ; +Nx ; mise +; moon ; +Volta ; Bun ; +Deno ; Node ; +distroless . + +## User scenarios + +The official image is a toolchain image. The scenarios it serves, in priority +order: + +1. **Build stage for app deployment (primary).** Used as `FROM ... AS build` in a + multi-stage Dockerfile. `vp install` + `vp build` produce the app; the exact + Node from `.node-version` is copied into a slim final stage. This is the + #1490 anchor. +2. **Container-native CI (primary).** GitLab CI, Buildkite, CircleCI, Jenkins + agents, Tekton, etc. set `image: ghcr.io/voidzero-dev/vite-plus:` and run + `vp install`, `vp check`, `vp test`, `vp build`. (GitHub Actions users are + already served by `setup-vp`, so this targets the rest of the ecosystem.) +3. **Reproducible dev environments (secondary).** Devcontainers, Codespaces, and + onboarding: a single image pins Node + package managers + vp so the toolchain + matches the repo with zero host setup. +4. **Ad-hoc / evaluation (secondary).** `docker run --rm -v $PWD:/app -w /app + ghcr.io/voidzero-dev/vite-plus vp ` to try vp or reproduce a bug report + on a clean toolchain. +5. **Platform / monorepo builders (secondary).** Internal PaaS and buildpack-style + systems standardizing on a canonical vp builder; monorepo single-app builds + (which motivate the future `vp prune --docker`). + +What it is explicitly **not**: the production runtime image. Shipping the full +toolchain (vite, rolldown, vitest, oxlint, ...) into a deployed container is the +bloat #1490 is complaining about. Production images are produced from the builder +via the documented multi-stage pattern. + +## Goals + +1. Publish a maintained, multi-arch (`linux/amd64`, `linux/arm64`) Vite+ + toolchain image on GHCR. +2. Honor `.node-version` automatically at build time via vp's existing managed + runtime, with no Node-version-specific image tags. +3. Document a recommended multi-stage pattern that produces a small production + image with the exact pinned Node and no vp. +4. Keep official, signature-verified glibc Node end to end (builder downloads it, + runtime copies it). +5. Provide patterns for the secondary scenarios (CI, devcontainer, static SPA, + ad-hoc). + +## Non-Goals + +1. A production runtime image (documented pattern instead, see Future Work for a + possible thin runtime base). +2. Node-version-keyed image tags (the tag sprawl this design avoids). +3. An Alpine/musl image variant in the first version. glibc is the v1 default + because it keeps official, signature-verified Node, avoids native-addon + breakage, and (via the multi-stage distroless final stage) is already smaller + than Alpine. Revisit in Future Work, gated on demand. See the rationale under + Future Work. +4. `vp prune --docker` monorepo pruning (Future Work). +5. Docker Hub publishing (GHCR only for now). + +## Design + +### Image role and version-alignment mechanism + +The image bundles `vp` and provisions Node at build time: + +1. In the build stage, `vp install` / `vp build` cause vp to read `.node-version` + (or `engines.node` / `devEngines.runtime`), download that exact official Node, + verify its PGP signature, and cache it under + `$VP_HOME/js_runtime/node//`. +2. The documented multi-stage pattern copies the resolved Node binary plus the + built app and production dependencies into a slim glibc final stage that does + not contain vp. + +This makes one image version-agnostic across every project's pinned Node, +eliminates the Corepack-in-Docker class of problems other tools hit, and keeps +deployed images small. + +### Base image, contents, and variants + +- **Base:** `debian:bookworm-slim` (glibc). Glibc is required so vp downloads the + official signature-verified Node and so native addons behave; debian-slim is + the consensus small glibc base (pnpm's choice) and provides the shell, `apt`, + and `git` that build/CI/dev scenarios need. +- **Preinstalled:** `vp` (on `PATH`), `ca-certificates`, `curl`, `git`, and a + build toolchain (`build-essential`, `python3`, `pkg-config`) for native addon + compilation (for example `better-sqlite3`). Package managers are handled by + vp's managed corepack/runtime, so they are provisioned per-project rather than + baked to a fixed version. +- **User:** create a non-root `vp` user (mirroring Bun's `USER bun` and Deno's + `USER deno`); document switching to root for steps that need `apt`. +- **Possible later variant:** a `-slim` toolchain image without the native build + toolchain for projects with no native deps (Future Work). + +### How `vp` gets into the image + +The published image is built hermetically from the release artifacts: the +per-arch `vp` binary produced by the existing release pipeline is copied into the +image, so the image version maps 1:1 to a `vp` release and needs no network at +image-build time to install vp itself. (The user-facing one-liner +`curl -fsSL https://vite.plus | VP_VERSION= bash` remains the documented way to +add vp to a custom base image.) + +### Tagging + +Tags track the `vp` version, not Node: + +- `ghcr.io/voidzero-dev/vite-plus:latest` +- `ghcr.io/voidzero-dev/vite-plus:` (for example `:1`) +- `ghcr.io/voidzero-dev/vite-plus:.` (for example `:1.4`) +- `ghcr.io/voidzero-dev/vite-plus:..` (for example `:1.4.2`) + +Users pin by exact tag or digest for reproducibility. No `node-` tags. + +### Security and reproducibility + +- Official, signature-verified glibc Node throughout (no unofficial musl builds). +- Non-root default user. +- Multi-arch manifest (`linux/amd64`, `linux/arm64`); vp already ships + `{x86_64,aarch64}-unknown-linux-gnu` binaries. +- Pinnable by digest. + +### Locating the resolved Node for the runtime stage + +No new CLI surface is required: `vp env which node` prints the resolved Node +binary path as its first (uncolored, pipe-friendly) line, and the runtime lives +at `$VP_HOME/js_runtime/node//bin/node`. The runtime stage copies that +file directly. + +### Publishing pipeline + +Add an image build/publish job to the release flow (`release.yml` / +`reusable-release-build.yml`) that builds the multi-arch image from the release +binaries and pushes to GHCR with the tag set above, gated on a successful +release. (Exact wiring is an implementation detail for the PR.) + +## Recommended Dockerfile patterns (documented for users) + +### 1. SSR / Node-server app, slim runtime (the #1490 case) + +```dockerfile +# syntax=docker/dockerfile:1 + +# --- build stage: official Vite+ toolchain image --- +FROM ghcr.io/voidzero-dev/vite-plus:1 AS build +WORKDIR /app + +# Dependency layer first for cache reuse. +COPY package.json pnpm-lock.yaml .node-version ./ +RUN vp install --frozen-lockfile + +# Build. vp reads .node-version and provisions that exact Node automatically. +COPY . . +RUN vp build + +# Production-only deps + the exact resolved Node binary for the runtime stage. +RUN vp install --frozen-lockfile --prod \ + && cp "$(vp env which node | head -1)" /tmp/node + +# --- runtime stage: small, glibc, no vp --- +FROM debian:bookworm-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production + +# Exact Node from .node-version (official, signature-verified glibc build). +COPY --from=build /tmp/node /usr/local/bin/node + +COPY --from=build /app/dist ./dist +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/package.json ./ + +USER nobody +EXPOSE 3000 +CMD ["node", "dist/server.js"] +``` + +The deployed image carries only Node + app, matches `.node-version` exactly, and +is far smaller than `node:24-alpine`. A distroless final base +(`gcr.io/distroless/cc`) is a documented size/security upgrade for users who do +not need a shell at runtime (see Future Work). + +### 2. Static SPA / SSG + +```dockerfile +FROM ghcr.io/voidzero-dev/vite-plus:1 AS build +WORKDIR /app +COPY package.json pnpm-lock.yaml .node-version ./ +RUN vp install --frozen-lockfile +COPY . . +RUN vp build + +FROM nginx:alpine AS runtime +COPY --from=build /app/dist /usr/share/nginx/html +``` + +No Node at runtime; the vp image is only the builder. + +### 3. Container-native CI + +```yaml +# e.g. GitLab CI +build: + image: ghcr.io/voidzero-dev/vite-plus:1 + script: + - vp install --frozen-lockfile + - vp check + - vp test + - vp build +``` + +### 4. Devcontainer + +```jsonc +// .devcontainer/devcontainer.json +{ + "image": "ghcr.io/voidzero-dev/vite-plus:1" +} +``` + +### 5. Ad-hoc / evaluation + +```bash +docker run --rm -it -v "$PWD:/app" -w /app ghcr.io/voidzero-dev/vite-plus vp build +``` + +## Open questions + +1. **Default app Node in the image.** The toolchain image bakes no specific app + Node (vp downloads the pinned version at build, needing network). Should we + offer a variant with an LTS Node prebaked for faster/offline builds, or rely + on caching and `VP_NODE_DIST_MIRROR`? (Leaning: no prebaked Node by default; + revisit with a prebaked or offline variant if demand appears.) +2. **Native build toolchain by default.** Include `build-essential`/`python3` in + the default image (larger, but native addons just work), or keep the default + lean and add them in a `-full` variant? (Leaning: include by default since + this is a builder image; offer a `-slim` later.) +3. **`vp install --prod` semantics for the runtime copy.** Confirm the exact flag + set vp exposes for a production-only install and whether a dedicated deps stage + improves layer caching in the documented pattern. +4. **Image naming.** `ghcr.io/voidzero-dev/vite-plus` vs a `-toolchain` suffix to + leave room for other images later. + +## Future Work + +1. **`vp prune --docker`** for monorepos: emit a target-scoped subset + (package.json files, pruned lockfile, source) so the dependency-install layer + caches across unrelated workspace edits, matching Turborepo `turbo prune` and + Nx `prune-lockfile`. This is the one capability plain package managers cannot + offer and the main reason monorepo Docker guides cite those tools. Likely its + own RFC. +2. **Distroless runtime guidance/variant.** Document (or provide) a + `gcr.io/distroless/cc` final stage and the `tini` PID-1 pattern for a smaller, + shell-less, better-CVE-posture runtime. +3. **Thin runtime base image.** Reconsider only if the documented copy-Node-in + pattern proves insufficient; would reintroduce Node-version coupling, so not + planned. +4. **Alpine/musl variant.** Deferred, not part of v1, and only worth adding on + real demand. The reasoning: + + - **Size does not motivate it here.** The two assumed wins for Alpine do not + apply to this design. On compressed size the glibc multi-stage path is + already smaller: `gcr.io/distroless/nodejs` ~45 MB and a distroless `cc` + base + copied Node beats `node:*-alpine` ~55 MB (`node:*-slim` ~75 MB). + #1490's "Alpine doubles the size" was bare-Alpine + `apk add nodejs` versus + `node:24-alpine`, a comparison the multi-stage glibc runtime sidesteps + entirely. + - **Two real costs.** (1) On musl, vp downloads Node from + `unofficial-builds.nodejs.org`, which publishes no PGP signature (see + `crates/vite_js_runtime/src/providers/node.rs`), so the musl variant breaks + this RFC's "official, signature-verified Node throughout" guarantee. (2) + musl is the classic native-addon sharp edge (prebuilt addons are usually + glibc; on musl they need musl prebuilds or source compilation plus + `gcompat`/`libc6-compat`), which Vite+ projects hit regularly (better-sqlite3, + sharp). The wider field treats musl as a hazard for the same reasons (Volta + unsupported on musl, mise needs `MISE_LIBC=musl`, moon needs + `MOON_TOOLCHAIN_FORCE_GLOBALS=true`, Turborepo `apk add libc6-compat`). + - **The one legitimate driver** is orgs that mandate Alpine everywhere: a + glibc-copied Node cannot load on a musl base, so those users are genuinely + excluded by v1. That narrow segment is what a future opt-in `-alpine`/musl + toolchain image would serve. + - **If added later**, ship it only as an opt-in variant with loud caveats + (unsigned unofficial Node; native addons may need `libc6-compat`/source + builds) and a documented libc autodetect/override. The release pipeline + already builds musl `vp` binaries, so the build lift is small; the ongoing + support burden is the real cost. +5. **Docker Hub publishing** for discoverability, in addition to GHCR. +6. **Offline / airgapped builds**: a prebaked-Node variant and `VP_NODE_DIST_MIRROR` + guidance. + +## References + +- Issue: [#1490](https://github.com/voidzero-dev/vite-plus/issues/1490) +- Q2 plan: [#1324](https://github.com/voidzero-dev/vite-plus/issues/1324) +- JS runtime management: [`js-runtime.md`](./js-runtime.md) +- Node signature verification: [`verify-node-shasums-signature.md`](./verify-node-shasums-signature.md) +- CI guide: `docs/guide/ci.md` +- Distribution prior art: pnpm , Deno , + mise , Turborepo + , distroless + . From 2760ea5192c1c13fa081600a88bc9154d742d22b Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 13:58:28 +0800 Subject: [PATCH 02/18] docs(rfc): reflect install-script image build in Docker RFC 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. --- rfcs/docker-image.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/rfcs/docker-image.md b/rfcs/docker-image.md index 0517f2eb45..76016aa4fc 100644 --- a/rfcs/docker-image.md +++ b/rfcs/docker-image.md @@ -2,7 +2,7 @@ - Issue: [#1490](https://github.com/voidzero-dev/vite-plus/issues/1490) - Plan: [#1324](https://github.com/voidzero-dev/vite-plus/issues/1324) ("Distribute `vp` across Homebrew, Windows installer, Docker image, apt etc.") -- Status: Draft +- Status: Accepted (implementation in progress) ## Summary @@ -201,12 +201,23 @@ deployed images small. ### How `vp` gets into the image -The published image is built hermetically from the release artifacts: the -per-arch `vp` binary produced by the existing release pipeline is copied into the -image, so the image version maps 1:1 to a `vp` release and needs no network at -image-build time to install vp itself. (The user-facing one-liner -`curl -fsSL https://vite.plus | VP_VERSION= bash` remains the documented way to -add vp to a custom base image.) +The image installs `vp` with the official install script, pinned to the release +version: + +```dockerfile +RUN curl -fsSL https://vite.plus | VP_VERSION="${VP_VERSION}" bash +``` + +The publish job runs after the npm release, so the pinned version is already on +the registry. This reuses the install script's battle-tested platform detection +(including correct gnu/musl and amd64/arm64 selection under buildx), so the same +Dockerfile produces every architecture without a per-arch artifact copy. The +image version maps 1:1 to a `vp` release via the `VP_VERSION` build arg, and the +same one-liner is the documented way to add `vp` to a custom base image. + +A fully hermetic build that copies the per-arch `vp` binary from the release +artifacts (no network at image-build time) is a possible later hardening; it is +not required for v1. ### Tagging From ea17dbe2f0c5286b611c9cd1bdec162be65a0072 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 13:58:28 +0800 Subject: [PATCH 03/18] feat(docker): add official Vite+ toolchain image 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 --- .github/workflows/release.yml | 52 ++++++++++++ docker/Dockerfile | 51 ++++++++++++ docs/.vitepress/config.mts | 1 + docs/guide/docker.md | 152 ++++++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+) create mode 100644 docker/Dockerfile create mode 100644 docs/guide/docker.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ca91858e8a..0618851117 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -232,3 +232,55 @@ jobs: • macOS/Linux: `curl -fsSL https://vite.plus | bash` • Windows: `irm https://vite.plus/ps1 | iex` embed-url: https://github.com/${{ github.repository }}/releases/tag/v${{ env.VERSION }} + + # Build and push the official toolchain Docker image to GHCR after the npm + # release is published (the image installs vp from npm, so the version must + # exist first). See docker/Dockerfile and docs/guide/docker.md. + publish-docker: + name: Publish Docker image + runs-on: ubuntu-latest + needs: [check, Release] + if: needs.check.outputs.version_changed == 'true' + permissions: + contents: read + packages: write + env: + VERSION: ${{ needs.check.outputs.version }} + IMAGE: ghcr.io/voidzero-dev/vite-plus + steps: + - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 + + - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + + - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + + - name: Log in to GHCR + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + with: + images: ${{ env.IMAGE }} + tags: | + type=semver,pattern={{version}},value=${{ env.VERSION }} + type=semver,pattern={{major}}.{{minor}},value=${{ env.VERSION }} + type=semver,pattern={{major}},value=${{ env.VERSION }} + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + with: + context: docker + file: docker/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VP_VERSION=${{ env.VERSION }} + provenance: false diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000000..79dbb70b7b --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,51 @@ +# syntax=docker/dockerfile:1 +# +# Official Vite+ toolchain image. +# +# Bundles the `vp` CLI for the build, CI, and development phases. This is NOT a +# production runtime image: it ships the full toolchain (vite, rolldown, vitest, +# oxlint, ...) and is meant for use as a build stage, CI image, or devcontainer. +# +# For production, use the documented multi-stage pattern (see docs/guide/docker.md) +# where this image builds the app and the exact Node.js resolved from +# `.node-version` is copied into a small, vp-free runtime stage. + +FROM debian:bookworm-slim + +LABEL org.opencontainers.image.source="https://github.com/voidzero-dev/vite-plus" \ + org.opencontainers.image.description="Vite+ toolchain image (vp CLI) for build, CI, and development" \ + org.opencontainers.image.licenses="MIT" + +# Version of vp to install. Override at build time: +# docker build --build-arg VP_VERSION=1.4.2 . +ARG VP_VERSION=latest + +# Toolchain image: include git and a C/C++ build toolchain so native addons +# (for example better-sqlite3 or sharp) can compile during `vp install`. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + git \ + xz-utils \ + build-essential \ + python3 \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* \ + && useradd --create-home --shell /bin/bash vp \ + && mkdir -p /app \ + && chown vp:vp /app + +# Run as a non-root user by default (mirrors oven/bun's `bun` and Deno's `deno`). +USER vp + +ENV VP_HOME=/home/vp/.vite-plus +ENV PATH=/home/vp/.vite-plus/bin:$PATH + +# Install the vp global CLI. The installer downloads the platform package from +# npm. Node.js itself is provisioned per-project by vp at build time, honoring +# `.node-version` / `engines.node` / `devEngines.runtime`. +RUN curl -fsSL https://vite.plus | VP_VERSION="${VP_VERSION}" bash \ + && vp --version + +WORKDIR /app diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index ba764c9e2f..dcf66686e7 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -72,6 +72,7 @@ const guideSidebar = [ items: [ { text: 'IDE Integration', link: '/guide/ide-integration' }, { text: 'CI', link: '/guide/ci' }, + { text: 'Docker', link: '/guide/docker' }, { text: 'Commit Hooks', link: '/guide/commit-hooks' }, { text: 'Monorepo Guide', link: '/guide/monorepo' }, { text: 'Troubleshooting', link: '/guide/troubleshooting' }, diff --git a/docs/guide/docker.md b/docs/guide/docker.md new file mode 100644 index 0000000000..fd819e6253 --- /dev/null +++ b/docs/guide/docker.md @@ -0,0 +1,152 @@ +# Docker + +Vite+ publishes an official Docker image that bundles the `vp` CLI for the +**build, CI, and development** phases. + +``` +ghcr.io/voidzero-dev/vite-plus +``` + +The image is a toolchain image, not a production runtime image. Because `vp` +already reads your project's pinned Node.js version (`.node-version`, +`engines.node`, or `devEngines.runtime`) and downloads that exact version, you do +not need a Node-version-specific base image: one image builds any project against +its own Node. + +For production, you do not ship this image. Instead you use a multi-stage build +where this image builds the app, and the exact Node.js it resolved is copied into +a small, vp-free runtime image. That keeps the deployed image small while +matching your project's Node version exactly. + +## Image tags + +Tags track the `vp` version: + +| Tag | Meaning | +| -------------------------------------------- | ---------------------- | +| `ghcr.io/voidzero-dev/vite-plus:latest` | Latest release | +| `ghcr.io/voidzero-dev/vite-plus:1` | Latest 1.x | +| `ghcr.io/voidzero-dev/vite-plus:1.4` | Latest 1.4.x | +| `ghcr.io/voidzero-dev/vite-plus:1.4.2` | Exact version | + +Pin an exact tag (or a digest) for reproducible builds. The image is published +for `linux/amd64` and `linux/arm64` and runs as a non-root user by default. + +## Production: SSR / Node-server app + +For apps that run Node.js in production (SvelteKit, Nuxt, a custom Vite SSR +server, and so on), build with the toolchain image and copy the resolved Node.js +and the built app into a slim runtime stage: + +```dockerfile [Dockerfile] +# syntax=docker/dockerfile:1 + +# --- build stage: the official Vite+ toolchain image --- +FROM ghcr.io/voidzero-dev/vite-plus:1 AS build +WORKDIR /app + +# Install dependencies first so this layer is cached across source changes. +COPY package.json pnpm-lock.yaml .node-version ./ +RUN vp install --frozen-lockfile + +# Build. vp reads .node-version and provisions that exact Node.js automatically. +COPY . . +RUN vp build + +# Stage production-only dependencies and the exact resolved Node.js binary. +RUN vp install --frozen-lockfile --prod \ + && cp "$(vp env which node | head -1)" /tmp/node + +# --- runtime stage: small, glibc, no vp --- +FROM debian:bookworm-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production + +# The exact Node.js from .node-version (official, signature-verified build). +COPY --from=build /tmp/node /usr/local/bin/node + +COPY --from=build /app/dist ./dist +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/package.json ./ + +USER nobody +EXPOSE 3000 +CMD ["node", "dist/server.js"] +``` + +The deployed image contains only Node.js plus your app, matches `.node-version` +exactly, and is smaller than a full `node:*` base image. + +::: tip Smaller still +For a shell-less, minimal-CVE runtime, swap the runtime base for distroless +(`gcr.io/distroless/cc`) and keep an `ENTRYPOINT` in vector form. It is glibc +based, so the copied Node.js binary remains compatible. +::: + +## Production: static SPA / SSG + +A static site needs no Node.js at runtime; serve the build output with any static +server: + +```dockerfile [Dockerfile] +FROM ghcr.io/voidzero-dev/vite-plus:1 AS build +WORKDIR /app +COPY package.json pnpm-lock.yaml .node-version ./ +RUN vp install --frozen-lockfile +COPY . . +RUN vp build + +FROM nginx:alpine AS runtime +COPY --from=build /app/dist /usr/share/nginx/html +``` + +## Continuous integration + +Use the image directly in container-based CI (GitLab CI, Buildkite, CircleCI, +Jenkins, and others): + +```yaml [.gitlab-ci.yml] +build: + image: ghcr.io/voidzero-dev/vite-plus:1 + script: + - vp install --frozen-lockfile + - vp check + - vp test + - vp build +``` + +On GitHub Actions, prefer [`setup-vp`](./ci) instead of the image. + +## Devcontainers + +Use the image as a ready-to-go development container with the toolchain +preinstalled: + +```jsonc [.devcontainer/devcontainer.json] +{ + "image": "ghcr.io/voidzero-dev/vite-plus:1" +} +``` + +## Ad-hoc usage + +Run any `vp` command against a project without installing vp on your machine: + +```bash +docker run --rm -it -v "$PWD:/app" -w /app ghcr.io/voidzero-dev/vite-plus vp build +``` + +## Notes + +- **Node.js version**: the image provisions the version from `.node-version` / + `engines.node` / `devEngines.runtime` at build time. There is no need to pick a + Node-specific image tag. +- **Native addons**: the image includes a C/C++ build toolchain (`build-essential`, + `python3`), so native dependencies such as `better-sqlite3` compile during + `vp install`. +- **glibc**: the image is glibc based so it can use the official, + signature-verified Node.js builds. An Alpine/musl variant is not currently + provided. +- **Custom base image**: to add `vp` to your own base image instead, run the + installer: `curl -fsSL https://vite.plus | bash` (set `VP_VERSION` to pin a + version). From 1e6450b8b51d09660fd03950fbfcdb1323a079f4 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 14:10:15 +0800 Subject: [PATCH 04/18] ci(docker): build a preview image from pkg.pr.new 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-, 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 --- .github/workflows/publish-to-pkg.pr.new.yml | 54 +++++++++++++++++++++ docker/Dockerfile | 12 +++-- rfcs/docker-image.md | 9 ++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-to-pkg.pr.new.yml b/.github/workflows/publish-to-pkg.pr.new.yml index 79279257c2..9662fe991d 100644 --- a/.github/workflows/publish-to-pkg.pr.new.yml +++ b/.github/workflows/publish-to-pkg.pr.new.yml @@ -137,3 +137,57 @@ jobs: './packages/cli' \ './packages/core' \ './packages/prompts' + + # Build and push a preview Docker image from the pkg.pr.new build so the image + # can be verified before a real release. Tagged `pr-`; never `latest`. + # See docker/Dockerfile and docs/guide/docker.md. + publish-docker-preview: + if: >- + github.repository == 'voidzero-dev/vite-plus' && + contains(github.event.pull_request.labels.*.name, 'pkg.pr.new') + name: Docker preview image + runs-on: ubuntu-latest + needs: publish + permissions: + contents: read + packages: write + env: + IMAGE: ghcr.io/voidzero-dev/vite-plus + PR_NUMBER: ${{ github.event.pull_request.number }} + steps: + - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 + + - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + + - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + + - name: Log in to GHCR + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Builds from the pkg.pr.new packages for this PR (VP_PR_VERSION). The + # platform packages must exist first, hence `needs: publish`. + - name: Build and push preview image + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + with: + context: docker + file: docker/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ env.IMAGE }}:pr-${{ env.PR_NUMBER }} + build-args: | + VP_PR_VERSION=${{ env.PR_NUMBER }} + provenance: false + + - name: Summary + run: | + { + echo "### Docker preview image" + echo "" + echo '```bash' + echo "docker pull ${IMAGE}:pr-${PR_NUMBER}" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/docker/Dockerfile b/docker/Dockerfile index 79dbb70b7b..fe36aa5a46 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,6 +20,11 @@ LABEL org.opencontainers.image.source="https://github.com/voidzero-dev/vite-plus # docker build --build-arg VP_VERSION=1.4.2 . ARG VP_VERSION=latest +# Optional: build a preview image from a pkg.pr.new build instead of npm. +# Set to a PR number or commit SHA; when set it overrides VP_VERSION. +# docker build --build-arg VP_PR_VERSION=1569 . +ARG VP_PR_VERSION= + # Toolchain image: include git and a C/C++ build toolchain so native addons # (for example better-sqlite3 or sharp) can compile during `vp install`. RUN apt-get update \ @@ -43,9 +48,10 @@ ENV VP_HOME=/home/vp/.vite-plus ENV PATH=/home/vp/.vite-plus/bin:$PATH # Install the vp global CLI. The installer downloads the platform package from -# npm. Node.js itself is provisioned per-project by vp at build time, honoring -# `.node-version` / `engines.node` / `devEngines.runtime`. -RUN curl -fsSL https://vite.plus | VP_VERSION="${VP_VERSION}" bash \ +# npm (or from pkg.pr.new when VP_PR_VERSION is set). Node.js itself is +# provisioned per-project by vp at build time, honoring `.node-version` / +# `engines.node` / `devEngines.runtime`. +RUN curl -fsSL https://vite.plus | VP_VERSION="${VP_VERSION}" VP_PR_VERSION="${VP_PR_VERSION}" bash \ && vp --version WORKDIR /app diff --git a/rfcs/docker-image.md b/rfcs/docker-image.md index 76016aa4fc..bed9231044 100644 --- a/rfcs/docker-image.md +++ b/rfcs/docker-image.md @@ -252,6 +252,15 @@ Add an image build/publish job to the release flow (`release.yml` / binaries and pushes to GHCR with the tag set above, gated on a successful release. (Exact wiring is an implementation detail for the PR.) +### Pre-release validation (preview image) + +To verify the image before a real release, the `pkg.pr.new` workflow +(`publish-to-pkg.pr.new.yml`) also builds the multi-arch image, but from the PR's +pkg.pr.new build (`VP_PR_VERSION`), and pushes it as `ghcr.io/voidzero-dev/vite-plus:pr-` +(never `latest`). This reuses the exact same `docker/Dockerfile` as the release, +so labeling a PR with `pkg.pr.new` produces a pullable preview image that +exercises the real build path. + ## Recommended Dockerfile patterns (documented for users) ### 1. SSR / Node-server app, slim runtime (the #1490 case) From 49084e393ee45e6bb9de57b68034c250c8975ad5 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 14:12:45 +0800 Subject: [PATCH 05/18] style(docker): apply oxfmt formatting to Docker docs and RFC Fixes vp check formatting failures in docs/guide/docker.md and rfcs/docker-image.md (table alignment and emphasis markers). --- docs/guide/docker.md | 14 +++++++------- rfcs/docker-image.md | 35 ++++++++++++++++++----------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/docs/guide/docker.md b/docs/guide/docker.md index fd819e6253..9562217b3c 100644 --- a/docs/guide/docker.md +++ b/docs/guide/docker.md @@ -22,12 +22,12 @@ matching your project's Node version exactly. Tags track the `vp` version: -| Tag | Meaning | -| -------------------------------------------- | ---------------------- | -| `ghcr.io/voidzero-dev/vite-plus:latest` | Latest release | -| `ghcr.io/voidzero-dev/vite-plus:1` | Latest 1.x | -| `ghcr.io/voidzero-dev/vite-plus:1.4` | Latest 1.4.x | -| `ghcr.io/voidzero-dev/vite-plus:1.4.2` | Exact version | +| Tag | Meaning | +| --------------------------------------- | -------------- | +| `ghcr.io/voidzero-dev/vite-plus:latest` | Latest release | +| `ghcr.io/voidzero-dev/vite-plus:1` | Latest 1.x | +| `ghcr.io/voidzero-dev/vite-plus:1.4` | Latest 1.4.x | +| `ghcr.io/voidzero-dev/vite-plus:1.4.2` | Exact version | Pin an exact tag (or a digest) for reproducible builds. The image is published for `linux/amd64` and `linux/arm64` and runs as a non-root user by default. @@ -124,7 +124,7 @@ preinstalled: ```jsonc [.devcontainer/devcontainer.json] { - "image": "ghcr.io/voidzero-dev/vite-plus:1" + "image": "ghcr.io/voidzero-dev/vite-plus:1", } ``` diff --git a/rfcs/docker-image.md b/rfcs/docker-image.md index bed9231044..0716917cc2 100644 --- a/rfcs/docker-image.md +++ b/rfcs/docker-image.md @@ -48,7 +48,7 @@ container Node aligned with `.node-version`. This RFC provides both. ### Why Vite+ is well positioned Every comparable tool delegates the Node version to the base `node:*` image tag -and only manages the *package manager* (via Corepack). Vite+ already manages the +and only manages the _package manager_ (via Corepack). Vite+ already manages the Node runtime itself: it reads the project's config and downloads the exact Node, verifying the official `SHASUMS256.txt.asc` PGP signature (see [`js-runtime.md`](./js-runtime.md) and @@ -61,20 +61,20 @@ version pinning with image tags. Researched against current official docs (2026-06-25). Summary of how comparable tools handle Node version + Docker: -| Tool | Official image | How the Node version is set | Default base | musl/Alpine stance | -| ------------------- | ----------------------- | ----------------------------------------------- | ------------------- | ------------------------------------------- | -| Volta | no (community only) | `volta` field in package.json, auto-fetch | glibc only | unsupported (libc dependency) | -| mise | exists but "do not use" | `mise install` from `.tool-versions`/`mise.toml`| debian-slim | discouraged; needs `MISE_LIBC=musl` | -| proto / moon | no (moon docs only) | layered on top of `node:*` base | `node:latest` | needs `MOON_TOOLCHAIN_FORCE_GLOBALS=true` | -| asdf | no (community only) | `asdf install` from `.tool-versions` | community | per-plugin; glibc Node by default | -| pnpm | yes (`ghcr.io/pnpm/pnpm`, no Node) | base `node:*` tag + Corepack | debian-slim | not addressed | -| Yarn | no | base `node:*` tag + Corepack (`packageManager`) | `node:*` | n/a | -| Turborepo | no | base `node:*` tag; `turbo prune --docker` | `node:*-alpine` | adds `libc6-compat` | -| Nx | no | base `node:*` tag; `prune-lockfile` | `node:lts-alpine` | not addressed | -| Bun | yes (`oven/bun`) | own runtime | debian; offers distroless | not discussed | -| Deno | yes (Hub + GHCR) | own runtime; ships a `:bin` image to copy in | debian; offers distroless | non-root default | -| Node official | yes | the tag is the version | debian (`-slim`, `-alpine`) | warns musl breaks glibc apps | -| distroless nodejs | yes (`gcr.io/distroless/nodejsNN`) | copy artifacts in | debian/glibc, ~45MB | glibc only | +| Tool | Official image | How the Node version is set | Default base | musl/Alpine stance | +| ----------------- | ---------------------------------- | ------------------------------------------------ | --------------------------- | ----------------------------------------- | +| Volta | no (community only) | `volta` field in package.json, auto-fetch | glibc only | unsupported (libc dependency) | +| mise | exists but "do not use" | `mise install` from `.tool-versions`/`mise.toml` | debian-slim | discouraged; needs `MISE_LIBC=musl` | +| proto / moon | no (moon docs only) | layered on top of `node:*` base | `node:latest` | needs `MOON_TOOLCHAIN_FORCE_GLOBALS=true` | +| asdf | no (community only) | `asdf install` from `.tool-versions` | community | per-plugin; glibc Node by default | +| pnpm | yes (`ghcr.io/pnpm/pnpm`, no Node) | base `node:*` tag + Corepack | debian-slim | not addressed | +| Yarn | no | base `node:*` tag + Corepack (`packageManager`) | `node:*` | n/a | +| Turborepo | no | base `node:*` tag; `turbo prune --docker` | `node:*-alpine` | adds `libc6-compat` | +| Nx | no | base `node:*` tag; `prune-lockfile` | `node:lts-alpine` | not addressed | +| Bun | yes (`oven/bun`) | own runtime | debian; offers distroless | not discussed | +| Deno | yes (Hub + GHCR) | own runtime; ships a `:bin` image to copy in | debian; offers distroless | non-root default | +| Node official | yes | the tag is the version | debian (`-slim`, `-alpine`) | warns musl breaks glibc apps | +| distroless nodejs | yes (`gcr.io/distroless/nodejsNN`) | copy artifacts in | debian/glibc, ~45MB | glibc only | Key takeaways that shape this RFC: @@ -128,7 +128,7 @@ order: onboarding: a single image pins Node + package managers + vp so the toolchain matches the repo with zero host setup. 4. **Ad-hoc / evaluation (secondary).** `docker run --rm -v $PWD:/app -w /app - ghcr.io/voidzero-dev/vite-plus vp ` to try vp or reproduce a bug report +ghcr.io/voidzero-dev/vite-plus vp ` to try vp or reproduce a bug report on a clean toolchain. 5. **Platform / monorepo builders (secondary).** Internal PaaS and buildpack-style systems standardizing on a canonical vp builder; monorepo single-app builds @@ -340,7 +340,7 @@ build: ```jsonc // .devcontainer/devcontainer.json { - "image": "ghcr.io/voidzero-dev/vite-plus:1" + "image": "ghcr.io/voidzero-dev/vite-plus:1", } ``` @@ -410,6 +410,7 @@ docker run --rm -it -v "$PWD:/app" -w /app ghcr.io/voidzero-dev/vite-plus vp bui builds) and a documented libc autodetect/override. The release pipeline already builds musl `vp` binaries, so the build lift is small; the ongoing support burden is the real cost. + 5. **Docker Hub publishing** for discoverability, in addition to GHCR. 6. **Offline / airgapped builds**: a prebaked-Node variant and `VP_NODE_DIST_MIRROR` guidance. From adcdbf602cbe4c7a6b6ffda6275eaf67c79d1c77 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 14:23:00 +0800 Subject: [PATCH 06/18] refactor(docker): simplify image and speed up preview build - 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. --- .github/workflows/publish-to-pkg.pr.new.yml | 4 +++- docker/Dockerfile | 9 +++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish-to-pkg.pr.new.yml b/.github/workflows/publish-to-pkg.pr.new.yml index 9662fe991d..87c25166bc 100644 --- a/.github/workflows/publish-to-pkg.pr.new.yml +++ b/.github/workflows/publish-to-pkg.pr.new.yml @@ -170,12 +170,14 @@ jobs: # Builds from the pkg.pr.new packages for this PR (VP_PR_VERSION). The # platform packages must exist first, hence `needs: publish`. + # amd64-only: this throwaway preview avoids the slow arm64 QEMU leg; arm64 + # is covered by the release build and the test-install-sh-arm64 job. - name: Build and push preview image uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: docker file: docker/Dockerfile - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 push: true tags: ${{ env.IMAGE }}:pr-${{ env.PR_NUMBER }} build-args: | diff --git a/docker/Dockerfile b/docker/Dockerfile index fe36aa5a46..0c9e0cf59f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -32,20 +32,17 @@ RUN apt-get update \ ca-certificates \ curl \ git \ - xz-utils \ build-essential \ python3 \ pkg-config \ && rm -rf /var/lib/apt/lists/* \ - && useradd --create-home --shell /bin/bash vp \ - && mkdir -p /app \ - && chown vp:vp /app + && useradd --create-home --shell /bin/bash vp # Run as a non-root user by default (mirrors oven/bun's `bun` and Deno's `deno`). USER vp -ENV VP_HOME=/home/vp/.vite-plus -ENV PATH=/home/vp/.vite-plus/bin:$PATH +ENV VP_HOME=/home/vp/.vite-plus \ + PATH=/home/vp/.vite-plus/bin:$PATH # Install the vp global CLI. The installer downloads the platform package from # npm (or from pkg.pr.new when VP_PR_VERSION is set). Node.js itself is From 1250709b78e91f76501fc817684f6bc9ca5ac768 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 14:59:25 +0800 Subject: [PATCH 07/18] docs(rfc): link the docs-example verification repo Reference why-reproductions-are-required/vite-plus-docker-example, which CI-verifies the documented Dockerfile patterns end to end. --- rfcs/docker-image.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rfcs/docker-image.md b/rfcs/docker-image.md index 0716917cc2..fe0fe9eb48 100644 --- a/rfcs/docker-image.md +++ b/rfcs/docker-image.md @@ -261,6 +261,15 @@ pkg.pr.new build (`VP_PR_VERSION`), and pushes it as `ghcr.io/voidzero-dev/vite- so labeling a PR with `pkg.pr.new` produces a pullable preview image that exercises the real build path. +### Docs example verification + +The Dockerfile patterns documented below (and in `docs/guide/docker.md`) are kept +honest by a reproduction repo whose GitHub Actions build and smoke-test each +example end to end (build the image, run the container, assert `HTTP 200`, and +assert the SSR runtime Node.js matches the pinned `.node-version`): + +- + ## Recommended Dockerfile patterns (documented for users) ### 1. SSR / Node-server app, slim runtime (the #1490 case) From cd94960a9bb259a3a9357e4becc96955f6a36d0a Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 15:18:42 +0800 Subject: [PATCH 08/18] docs(docker): prune prod deps in a separate stage 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). --- docs/guide/docker.md | 29 +++++++++++++++++++++++------ rfcs/docker-image.md | 22 ++++++++++++++++------ 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/docs/guide/docker.md b/docs/guide/docker.md index 9562217b3c..43b5ba74e6 100644 --- a/docs/guide/docker.md +++ b/docs/guide/docker.md @@ -53,9 +53,17 @@ RUN vp install --frozen-lockfile COPY . . RUN vp build -# Stage production-only dependencies and the exact resolved Node.js binary. -RUN vp install --frozen-lockfile --prod \ - && cp "$(vp env which node | head -1)" /tmp/node +# Export the exact resolved Node.js binary for the runtime stage. +RUN cp "$(vp env which node | head -1)" /tmp/node + +# --- deps stage: production-only dependencies --- +# A separate, fresh `--prod` install so devDependencies (including the vite-plus +# toolchain) are excluded. Running `--prod` over the full install above would not +# prune the already-installed devDependencies. +FROM ghcr.io/voidzero-dev/vite-plus:1 AS deps +WORKDIR /app +COPY package.json pnpm-lock.yaml .node-version ./ +RUN vp install --frozen-lockfile --prod # --- runtime stage: small, glibc, no vp --- FROM debian:bookworm-slim AS runtime @@ -66,7 +74,7 @@ ENV NODE_ENV=production COPY --from=build /tmp/node /usr/local/bin/node COPY --from=build /app/dist ./dist -COPY --from=build /app/node_modules ./node_modules +COPY --from=deps /app/node_modules ./node_modules COPY --from=build /app/package.json ./ USER nobody @@ -74,8 +82,17 @@ EXPOSE 3000 CMD ["node", "dist/server.js"] ``` -The deployed image contains only Node.js plus your app, matches `.node-version` -exactly, and is smaller than a full `node:*` base image. +The deployed image contains only Node.js plus your app and production +dependencies, and matches `.node-version` exactly. It is much smaller than the +default `node:*` image; see the distroless tip below for the smallest result. + +::: warning Prune production dependencies in a separate stage +Install production dependencies in their own `deps` stage as shown. Running +`vp install --prod` after a full `vp install` in the same stage does not remove +the already-installed devDependencies, so the `vite-plus` toolchain would be +copied into the runtime image. If your server bundle is fully self-contained (no +un-bundled runtime dependencies), you can skip copying `node_modules` entirely. +::: ::: tip Smaller still For a shell-less, minimal-CVE runtime, swap the runtime base for distroless diff --git a/rfcs/docker-image.md b/rfcs/docker-image.md index fe0fe9eb48..b053e66fe9 100644 --- a/rfcs/docker-image.md +++ b/rfcs/docker-image.md @@ -289,9 +289,15 @@ RUN vp install --frozen-lockfile COPY . . RUN vp build -# Production-only deps + the exact resolved Node binary for the runtime stage. -RUN vp install --frozen-lockfile --prod \ - && cp "$(vp env which node | head -1)" /tmp/node +# Export the exact resolved Node binary for the runtime stage. +RUN cp "$(vp env which node | head -1)" /tmp/node + +# --- deps stage: production-only dependencies (fresh --prod, so devDeps are +# excluded; running --prod over the full install above would not prune them) --- +FROM ghcr.io/voidzero-dev/vite-plus:1 AS deps +WORKDIR /app +COPY package.json pnpm-lock.yaml .node-version ./ +RUN vp install --frozen-lockfile --prod # --- runtime stage: small, glibc, no vp --- FROM debian:bookworm-slim AS runtime @@ -302,7 +308,7 @@ ENV NODE_ENV=production COPY --from=build /tmp/node /usr/local/bin/node COPY --from=build /app/dist ./dist -COPY --from=build /app/node_modules ./node_modules +COPY --from=deps /app/node_modules ./node_modules COPY --from=build /app/package.json ./ USER nobody @@ -310,8 +316,12 @@ EXPOSE 3000 CMD ["node", "dist/server.js"] ``` -The deployed image carries only Node + app, matches `.node-version` exactly, and -is far smaller than `node:24-alpine`. A distroless final base +The deployed image carries only Node + app + production deps, matches +`.node-version` exactly, and is much smaller than the default `node:*` image. +Production dependencies must be installed in a separate `deps` stage: running +`vp install --prod` over the full install in the build stage does not prune the +already-installed devDependencies (the large `vite-plus` toolchain), so it would +otherwise be copied into the runtime. A distroless final base (`gcr.io/distroless/cc`) is a documented size/security upgrade for users who do not need a shell at runtime (see Future Work). From f546bc9bce5dc8c5d1af4ea85e29126baab1260b Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 17:04:00 +0800 Subject: [PATCH 09/18] feat(docker): drop the baked default Node from the toolchain image 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. --- docker/Dockerfile | 8 +++++++- rfcs/docker-image.md | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 0c9e0cf59f..146743c6e1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -48,7 +48,13 @@ ENV VP_HOME=/home/vp/.vite-plus \ # npm (or from pkg.pr.new when VP_PR_VERSION is set). Node.js itself is # provisioned per-project by vp at build time, honoring `.node-version` / # `engines.node` / `devEngines.runtime`. +# +# The installer pre-provisions a default Node.js (~190 MB). Drop it: each project +# downloads its own pinned Node at build time, so the default is dead weight in a +# builder image. The node/npm/npx shims remain and fetch the right version on +# first use. RUN curl -fsSL https://vite.plus | VP_VERSION="${VP_VERSION}" VP_PR_VERSION="${VP_PR_VERSION}" bash \ - && vp --version + && vp --version \ + && rm -rf "$VP_HOME/js_runtime" WORKDIR /app diff --git a/rfcs/docker-image.md b/rfcs/docker-image.md index b053e66fe9..7c7ba8986f 100644 --- a/rfcs/docker-image.md +++ b/rfcs/docker-image.md @@ -194,6 +194,12 @@ deployed images small. compilation (for example `better-sqlite3`). Package managers are handled by vp's managed corepack/runtime, so they are provisioned per-project rather than baked to a fixed version. +- **No baked default Node.js:** the installer pre-provisions a default Node.js + (~190 MB); the image drops it (`rm -rf $VP_HOME/js_runtime`) because each + project provisions its own pinned Node at build time, so a default is dead + weight in a builder. The `node`/`npm`/`npx` shims remain and fetch the right + version on first use. This keeps the toolchain image ~190 MB smaller, more than + a switch to Alpine/musl would save (and without the musl tradeoffs). - **User:** create a non-root `vp` user (mirroring Bun's `USER bun` and Deno's `USER deno`); document switching to root for steps that need `apt`. - **Possible later variant:** a `-slim` toolchain image without the native build From ea9542e37eb0561dadf3c45c62a4be19b11335b6 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 17:24:30 +0800 Subject: [PATCH 10/18] feat(docker): add Alpine (musl) toolchain image variant 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. --- .github/workflows/publish-to-pkg.pr.new.yml | 26 +++++---- .github/workflows/release.yml | 19 ++++++- docker/Dockerfile.alpine | 53 ++++++++++++++++++ docs/guide/docker.md | 56 +++++++++++++++++-- rfcs/docker-image.md | 60 +++++++++++---------- 5 files changed, 171 insertions(+), 43 deletions(-) create mode 100644 docker/Dockerfile.alpine diff --git a/.github/workflows/publish-to-pkg.pr.new.yml b/.github/workflows/publish-to-pkg.pr.new.yml index 87c25166bc..e2960e8589 100644 --- a/.github/workflows/publish-to-pkg.pr.new.yml +++ b/.github/workflows/publish-to-pkg.pr.new.yml @@ -145,20 +145,28 @@ jobs: if: >- github.repository == 'voidzero-dev/vite-plus' && contains(github.event.pull_request.labels.*.name, 'pkg.pr.new') - name: Docker preview image + name: Docker preview image (${{ matrix.variant }}) runs-on: ubuntu-latest needs: publish permissions: contents: read packages: write + strategy: + fail-fast: false + matrix: + include: + - variant: debian + dockerfile: docker/Dockerfile + tag_suffix: '' + - variant: alpine + dockerfile: docker/Dockerfile.alpine + tag_suffix: '-alpine' env: IMAGE: ghcr.io/voidzero-dev/vite-plus - PR_NUMBER: ${{ github.event.pull_request.number }} + TAG: pr-${{ github.event.pull_request.number }}${{ matrix.tag_suffix }} steps: - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 - - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Log in to GHCR @@ -176,20 +184,20 @@ jobs: uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: docker - file: docker/Dockerfile + file: ${{ matrix.dockerfile }} platforms: linux/amd64 push: true - tags: ${{ env.IMAGE }}:pr-${{ env.PR_NUMBER }} + tags: ${{ env.IMAGE }}:${{ env.TAG }} build-args: | - VP_PR_VERSION=${{ env.PR_NUMBER }} + VP_PR_VERSION=${{ github.event.pull_request.number }} provenance: false - name: Summary run: | { - echo "### Docker preview image" + echo "### Docker preview image (${{ matrix.variant }})" echo "" echo '```bash' - echo "docker pull ${IMAGE}:pr-${PR_NUMBER}" + echo "docker pull ${IMAGE}:${TAG}" echo '```' } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0618851117..8b5600c236 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -237,13 +237,25 @@ jobs: # release is published (the image installs vp from npm, so the version must # exist first). See docker/Dockerfile and docs/guide/docker.md. publish-docker: - name: Publish Docker image + name: Publish Docker image (${{ matrix.variant }}) runs-on: ubuntu-latest needs: [check, Release] if: needs.check.outputs.version_changed == 'true' permissions: contents: read packages: write + strategy: + fail-fast: false + matrix: + include: + # Debian (glibc) is the default: tags get no suffix. + - variant: debian + dockerfile: docker/Dockerfile + suffix: '' + # Alpine (musl): tags get the -alpine suffix. + - variant: alpine + dockerfile: docker/Dockerfile.alpine + suffix: '-alpine' env: VERSION: ${{ needs.check.outputs.version }} IMAGE: ghcr.io/voidzero-dev/vite-plus @@ -266,6 +278,9 @@ jobs: uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ${{ env.IMAGE }} + flavor: | + latest=false + suffix=${{ matrix.suffix }} tags: | type=semver,pattern={{version}},value=${{ env.VERSION }} type=semver,pattern={{major}}.{{minor}},value=${{ env.VERSION }} @@ -276,7 +291,7 @@ jobs: uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: docker - file: docker/Dockerfile + file: ${{ matrix.dockerfile }} platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine new file mode 100644 index 0000000000..f808d0dc92 --- /dev/null +++ b/docker/Dockerfile.alpine @@ -0,0 +1,53 @@ +# syntax=docker/dockerfile:1 +# +# Alpine (musl) variant of the official Vite+ toolchain image. +# +# Smaller base than the Debian image, but note the tradeoffs: +# - Vite+ installs Node.js from the unofficial musl builds +# (unofficial-builds.nodejs.org), which are NOT PGP-signed. +# - Some native addons need musl prebuilds or source compilation. +# - A musl Node.js binary only runs on a musl base, so pair this builder with +# an Alpine runtime stage (see docs/guide/docker.md). +# +# Prefer the Debian image (ghcr.io/voidzero-dev/vite-plus) unless you +# specifically need Alpine/musl. + +FROM alpine:3.21 + +LABEL org.opencontainers.image.source="https://github.com/voidzero-dev/vite-plus" \ + org.opencontainers.image.description="Vite+ toolchain image (vp CLI, Alpine/musl) for build, CI, and development" \ + org.opencontainers.image.licenses="MIT" + +# Version of vp to install. Override at build time: +# docker build --build-arg VP_VERSION=1.4.2 -f docker/Dockerfile.alpine . +ARG VP_VERSION=latest + +# Optional: build a preview image from a pkg.pr.new build instead of npm. +ARG VP_PR_VERSION= + +# build-base + python3 let native addons compile from source on musl. +RUN apk add --no-cache \ + bash \ + ca-certificates \ + curl \ + git \ + tar \ + build-base \ + python3 \ + && adduser -D -s /bin/bash vp + +# Run as a non-root user by default (mirrors oven/bun's `bun` and Deno's `deno`). +USER vp + +ENV VP_HOME=/home/vp/.vite-plus \ + PATH=/home/vp/.vite-plus/bin:$PATH + +# Install the vp global CLI (musl build), then drop the pre-provisioned default +# Node.js: each project provisions its own pinned Node at build time, so the +# default is dead weight. The node/npm/npx shims remain and fetch the right +# version on first use. +RUN curl -fsSL https://vite.plus | VP_VERSION="${VP_VERSION}" VP_PR_VERSION="${VP_PR_VERSION}" bash \ + && vp --version \ + && rm -rf "$VP_HOME/js_runtime" + +WORKDIR /app diff --git a/docs/guide/docker.md b/docs/guide/docker.md index 43b5ba74e6..c95202bb79 100644 --- a/docs/guide/docker.md +++ b/docs/guide/docker.md @@ -32,6 +32,11 @@ Tags track the `vp` version: Pin an exact tag (or a digest) for reproducible builds. The image is published for `linux/amd64` and `linux/arm64` and runs as a non-root user by default. +The default image is Debian (glibc). An Alpine (musl) variant is published under +the same versions with an `-alpine` suffix (`:latest-alpine`, `:1-alpine`, +`:1.4-alpine`, `:1.4.2-alpine`). See [Alpine variant](#alpine-musl-variant) for +when to use it and its tradeoffs. + ## Production: SSR / Node-server app For apps that run Node.js in production (SvelteKit, Nuxt, a custom Vite SSR @@ -153,6 +158,51 @@ Run any `vp` command against a project without installing vp on your machine: docker run --rm -it -v "$PWD:/app" -w /app ghcr.io/voidzero-dev/vite-plus vp build ``` +## Alpine (musl) variant + +The `-alpine` tags are a musl build for teams that standardize on Alpine. They +produce the smallest runtime image, but come with tradeoffs: + +- Vite+ installs Node.js from the **unofficial musl builds** + (`unofficial-builds.nodejs.org`), which are **not PGP-signed** (the Debian + image uses the official, signature-verified builds). +- Some native addons need musl prebuilds or source compilation. +- A musl Node.js binary only runs on a musl base, so the runtime stage must also + be Alpine (not `debian:bookworm-slim` or distroless). + +Prefer the default Debian image unless you specifically need Alpine. The SSR +pattern with an Alpine runtime: + +```dockerfile [Dockerfile] +# syntax=docker/dockerfile:1 + +FROM ghcr.io/voidzero-dev/vite-plus:1-alpine AS build +WORKDIR /app +COPY package.json pnpm-lock.yaml .node-version ./ +RUN vp install --frozen-lockfile +COPY . . +RUN vp build +RUN cp "$(vp env which node | head -1)" /tmp/node + +FROM ghcr.io/voidzero-dev/vite-plus:1-alpine AS deps +WORKDIR /app +COPY package.json pnpm-lock.yaml .node-version ./ +RUN vp install --frozen-lockfile --prod + +# Runtime must be a musl base so the musl Node.js binary runs. +FROM alpine:3.21 AS runtime +WORKDIR /app +ENV NODE_ENV=production +RUN apk add --no-cache libstdc++ +COPY --from=build /tmp/node /usr/local/bin/node +COPY --from=build /app/dist ./dist +COPY --from=deps /app/node_modules ./node_modules +COPY --from=build /app/package.json ./ +USER nobody +EXPOSE 3000 +CMD ["node", "dist/server.js"] +``` + ## Notes - **Node.js version**: the image provisions the version from `.node-version` / @@ -161,9 +211,9 @@ docker run --rm -it -v "$PWD:/app" -w /app ghcr.io/voidzero-dev/vite-plus vp bui - **Native addons**: the image includes a C/C++ build toolchain (`build-essential`, `python3`), so native dependencies such as `better-sqlite3` compile during `vp install`. -- **glibc**: the image is glibc based so it can use the official, - signature-verified Node.js builds. An Alpine/musl variant is not currently - provided. +- **glibc by default**: the default image is glibc based so it uses the official, + signature-verified Node.js builds. An [Alpine/musl variant](#alpine-musl-variant) + is also published (`-alpine` tags) with the tradeoffs noted above. - **Custom base image**: to add `vp` to your own base image instead, run the installer: `curl -fsSL https://vite.plus | bash` (set `VP_VERSION` to pin a version). diff --git a/rfcs/docker-image.md b/rfcs/docker-image.md index 7c7ba8986f..587df37b55 100644 --- a/rfcs/docker-image.md +++ b/rfcs/docker-image.md @@ -157,11 +157,10 @@ via the documented multi-stage pattern. 1. A production runtime image (documented pattern instead, see Future Work for a possible thin runtime base). 2. Node-version-keyed image tags (the tag sprawl this design avoids). -3. An Alpine/musl image variant in the first version. glibc is the v1 default - because it keeps official, signature-verified Node, avoids native-addon - breakage, and (via the multi-stage distroless final stage) is already smaller - than Alpine. Revisit in Future Work, gated on demand. See the rationale under - Future Work. +3. Alpine/musl as the _default_. glibc is the default (official, + signature-verified Node, no native-addon breakage). An Alpine/musl image is + published as an opt-in `-alpine` variant with documented caveats (see Design); + it is not the recommended default. 4. `vp prune --docker` monorepo pruning (Future Work). 5. Docker Hub publishing (GHCR only for now). @@ -202,6 +201,14 @@ deployed images small. a switch to Alpine/musl would save (and without the musl tradeoffs). - **User:** create a non-root `vp` user (mirroring Bun's `USER bun` and Deno's `USER deno`); document switching to root for steps that need `apt`. +- **Alpine/musl variant (opt-in):** an `alpine:3` (musl) toolchain image + (`docker/Dockerfile.alpine`), published under `-alpine` tags. It produces the + smallest runtime (an Alpine SSR runtime measured ~136 MB vs ~150 MB distroless + and ~198 MB debian-slim), but carries the musl tradeoffs: 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. Builds via the same matrix as the Debian image. - **Possible later variant:** a `-slim` toolchain image without the native build toolchain for projects with no native deps (Future Work). @@ -234,6 +241,9 @@ Tags track the `vp` version, not Node: - `ghcr.io/voidzero-dev/vite-plus:.` (for example `:1.4`) - `ghcr.io/voidzero-dev/vite-plus:..` (for example `:1.4.2`) +The Alpine variant publishes the same set with an `-alpine` suffix +(`:latest-alpine`, `:1-alpine`, `:1.4-alpine`, `:1.4.2-alpine`). + Users pin by exact tag or digest for reproducibility. No `node-` tags. ### Security and reproducibility @@ -406,35 +416,27 @@ docker run --rm -it -v "$PWD:/app" -w /app ghcr.io/voidzero-dev/vite-plus vp bui 3. **Thin runtime base image.** Reconsider only if the documented copy-Node-in pattern proves insufficient; would reintroduce Node-version coupling, so not planned. -4. **Alpine/musl variant.** Deferred, not part of v1, and only worth adding on - real demand. The reasoning: - - - **Size does not motivate it here.** The two assumed wins for Alpine do not - apply to this design. On compressed size the glibc multi-stage path is - already smaller: `gcr.io/distroless/nodejs` ~45 MB and a distroless `cc` - base + copied Node beats `node:*-alpine` ~55 MB (`node:*-slim` ~75 MB). - #1490's "Alpine doubles the size" was bare-Alpine + `apk add nodejs` versus - `node:24-alpine`, a comparison the multi-stage glibc runtime sidesteps - entirely. - - **Two real costs.** (1) On musl, vp downloads Node from - `unofficial-builds.nodejs.org`, which publishes no PGP signature (see - `crates/vite_js_runtime/src/providers/node.rs`), so the musl variant breaks - this RFC's "official, signature-verified Node throughout" guarantee. (2) - musl is the classic native-addon sharp edge (prebuilt addons are usually +4. **Alpine/musl variant (shipped as opt-in).** Published under `-alpine` tags + (`docker/Dockerfile.alpine`); the Debian image stays the default. It serves + teams that mandate Alpine and yields the smallest runtime (an Alpine SSR + runtime measured ~136 MB). It ships with loud caveats because the tradeoffs + are real: + + - On musl, vp downloads Node from `unofficial-builds.nodejs.org`, which + publishes no PGP signature (see `crates/vite_js_runtime/src/providers/node.rs`), + so the Alpine variant does not get the "official, signature-verified Node" + guarantee the Debian image has. + - musl is the classic native-addon sharp edge (prebuilt addons are usually glibc; on musl they need musl prebuilds or source compilation plus `gcompat`/`libc6-compat`), which Vite+ projects hit regularly (better-sqlite3, sharp). The wider field treats musl as a hazard for the same reasons (Volta unsupported on musl, mise needs `MISE_LIBC=musl`, moon needs `MOON_TOOLCHAIN_FORCE_GLOBALS=true`, Turborepo `apk add libc6-compat`). - - **The one legitimate driver** is orgs that mandate Alpine everywhere: a - glibc-copied Node cannot load on a musl base, so those users are genuinely - excluded by v1. That narrow segment is what a future opt-in `-alpine`/musl - toolchain image would serve. - - **If added later**, ship it only as an opt-in variant with loud caveats - (unsigned unofficial Node; native addons may need `libc6-compat`/source - builds) and a documented libc autodetect/override. The release pipeline - already builds musl `vp` binaries, so the build lift is small; the ongoing - support burden is the real cost. + - A musl Node binary only runs on a musl base, so the documented Alpine + multi-stage pattern uses an Alpine runtime stage (not debian-slim/distroless). + + Remaining follow-up: a documented libc autodetect/override and reducing the + ongoing support burden. 5. **Docker Hub publishing** for discoverability, in addition to GHCR. 6. **Offline / airgapped builds**: a prebaked-Node variant and `VP_NODE_DIST_MIRROR` From 8e331a69b9109449b182085f6ecce34d026e1b5a Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 17:33:25 +0800 Subject: [PATCH 11/18] docs(docker): note building a static SPA with the Alpine toolchain --- docs/guide/docker.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/guide/docker.md b/docs/guide/docker.md index c95202bb79..b7dc93ea70 100644 --- a/docs/guide/docker.md +++ b/docs/guide/docker.md @@ -203,6 +203,10 @@ EXPOSE 3000 CMD ["node", "dist/server.js"] ``` +For a static SPA there is no Node.js at runtime, so only the builder changes: +swap the build stage to `ghcr.io/voidzero-dev/vite-plus:1-alpine`; the +`nginx:alpine` runtime and its output are unchanged. + ## Notes - **Node.js version**: the image provisions the version from `.node-version` / From c49f12377cf5c11033310b6a29eb26d9d380013f Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 17:40:09 +0800 Subject: [PATCH 12/18] docs(docker): use 0.2.2 (first Docker release) in examples 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. --- docs/guide/docker.md | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/docs/guide/docker.md b/docs/guide/docker.md index b7dc93ea70..67714a2f2b 100644 --- a/docs/guide/docker.md +++ b/docs/guide/docker.md @@ -22,19 +22,22 @@ matching your project's Node version exactly. Tags track the `vp` version: -| Tag | Meaning | -| --------------------------------------- | -------------- | -| `ghcr.io/voidzero-dev/vite-plus:latest` | Latest release | -| `ghcr.io/voidzero-dev/vite-plus:1` | Latest 1.x | -| `ghcr.io/voidzero-dev/vite-plus:1.4` | Latest 1.4.x | -| `ghcr.io/voidzero-dev/vite-plus:1.4.2` | Exact version | +| Tag | Meaning | +| --------------------------------------- | ------------------------------------ | +| `ghcr.io/voidzero-dev/vite-plus:latest` | Latest release | +| `ghcr.io/voidzero-dev/vite-plus:0` | Latest 0.x | +| `ghcr.io/voidzero-dev/vite-plus:0.2` | Latest 0.2.x | +| `ghcr.io/voidzero-dev/vite-plus:0.2.2` | Exact version (first Docker release) | Pin an exact tag (or a digest) for reproducible builds. The image is published for `linux/amd64` and `linux/arm64` and runs as a non-root user by default. +Browse all published versions and digests on the GitHub package page: +. + The default image is Debian (glibc). An Alpine (musl) variant is published under -the same versions with an `-alpine` suffix (`:latest-alpine`, `:1-alpine`, -`:1.4-alpine`, `:1.4.2-alpine`). See [Alpine variant](#alpine-musl-variant) for +the same versions with an `-alpine` suffix (`:latest-alpine`, `:0-alpine`, +`:0.2-alpine`, `:0.2.2-alpine`). See [Alpine variant](#alpine-musl-variant) for when to use it and its tradeoffs. ## Production: SSR / Node-server app @@ -47,7 +50,7 @@ and the built app into a slim runtime stage: # syntax=docker/dockerfile:1 # --- build stage: the official Vite+ toolchain image --- -FROM ghcr.io/voidzero-dev/vite-plus:1 AS build +FROM ghcr.io/voidzero-dev/vite-plus:0.2.2 AS build WORKDIR /app # Install dependencies first so this layer is cached across source changes. @@ -65,7 +68,7 @@ RUN cp "$(vp env which node | head -1)" /tmp/node # A separate, fresh `--prod` install so devDependencies (including the vite-plus # toolchain) are excluded. Running `--prod` over the full install above would not # prune the already-installed devDependencies. -FROM ghcr.io/voidzero-dev/vite-plus:1 AS deps +FROM ghcr.io/voidzero-dev/vite-plus:0.2.2 AS deps WORKDIR /app COPY package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile --prod @@ -111,7 +114,7 @@ A static site needs no Node.js at runtime; serve the build output with any stati server: ```dockerfile [Dockerfile] -FROM ghcr.io/voidzero-dev/vite-plus:1 AS build +FROM ghcr.io/voidzero-dev/vite-plus:0.2.2 AS build WORKDIR /app COPY package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile @@ -129,7 +132,7 @@ Jenkins, and others): ```yaml [.gitlab-ci.yml] build: - image: ghcr.io/voidzero-dev/vite-plus:1 + image: ghcr.io/voidzero-dev/vite-plus:0.2.2 script: - vp install --frozen-lockfile - vp check @@ -146,7 +149,7 @@ preinstalled: ```jsonc [.devcontainer/devcontainer.json] { - "image": "ghcr.io/voidzero-dev/vite-plus:1", + "image": "ghcr.io/voidzero-dev/vite-plus:0.2.2", } ``` @@ -176,7 +179,7 @@ pattern with an Alpine runtime: ```dockerfile [Dockerfile] # syntax=docker/dockerfile:1 -FROM ghcr.io/voidzero-dev/vite-plus:1-alpine AS build +FROM ghcr.io/voidzero-dev/vite-plus:0.2.2-alpine AS build WORKDIR /app COPY package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile @@ -184,7 +187,7 @@ COPY . . RUN vp build RUN cp "$(vp env which node | head -1)" /tmp/node -FROM ghcr.io/voidzero-dev/vite-plus:1-alpine AS deps +FROM ghcr.io/voidzero-dev/vite-plus:0.2.2-alpine AS deps WORKDIR /app COPY package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile --prod @@ -204,7 +207,7 @@ CMD ["node", "dist/server.js"] ``` For a static SPA there is no Node.js at runtime, so only the builder changes: -swap the build stage to `ghcr.io/voidzero-dev/vite-plus:1-alpine`; the +swap the build stage to `ghcr.io/voidzero-dev/vite-plus:0.2.2-alpine`; the `nginx:alpine` runtime and its output are unchanged. ## Notes From 9d628e11cd21c75e78ad79d03aad74d0b3f1bb06 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 21:09:38 +0800 Subject: [PATCH 13/18] docs(docker): COPY --chown=vp:vp in multi-stage examples 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. --- docs/guide/docker.md | 19 +++++++++++-------- rfcs/docker-image.md | 20 +++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/docs/guide/docker.md b/docs/guide/docker.md index 67714a2f2b..249849f1b1 100644 --- a/docs/guide/docker.md +++ b/docs/guide/docker.md @@ -54,11 +54,11 @@ FROM ghcr.io/voidzero-dev/vite-plus:0.2.2 AS build WORKDIR /app # Install dependencies first so this layer is cached across source changes. -COPY package.json pnpm-lock.yaml .node-version ./ +COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile # Build. vp reads .node-version and provisions that exact Node.js automatically. -COPY . . +COPY --chown=vp:vp . . RUN vp build # Export the exact resolved Node.js binary for the runtime stage. @@ -70,7 +70,7 @@ RUN cp "$(vp env which node | head -1)" /tmp/node # prune the already-installed devDependencies. FROM ghcr.io/voidzero-dev/vite-plus:0.2.2 AS deps WORKDIR /app -COPY package.json pnpm-lock.yaml .node-version ./ +COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile --prod # --- runtime stage: small, glibc, no vp --- @@ -116,9 +116,9 @@ server: ```dockerfile [Dockerfile] FROM ghcr.io/voidzero-dev/vite-plus:0.2.2 AS build WORKDIR /app -COPY package.json pnpm-lock.yaml .node-version ./ +COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile -COPY . . +COPY --chown=vp:vp . . RUN vp build FROM nginx:alpine AS runtime @@ -181,15 +181,15 @@ pattern with an Alpine runtime: FROM ghcr.io/voidzero-dev/vite-plus:0.2.2-alpine AS build WORKDIR /app -COPY package.json pnpm-lock.yaml .node-version ./ +COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile -COPY . . +COPY --chown=vp:vp . . RUN vp build RUN cp "$(vp env which node | head -1)" /tmp/node FROM ghcr.io/voidzero-dev/vite-plus:0.2.2-alpine AS deps WORKDIR /app -COPY package.json pnpm-lock.yaml .node-version ./ +COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile --prod # Runtime must be a musl base so the musl Node.js binary runs. @@ -215,6 +215,9 @@ swap the build stage to `ghcr.io/voidzero-dev/vite-plus:0.2.2-alpine`; the - **Node.js version**: the image provisions the version from `.node-version` / `engines.node` / `devEngines.runtime` at build time. There is no need to pick a Node-specific image tag. +- **Non-root user**: the image runs as the non-root `vp` user, so copy sources + with `COPY --chown=vp:vp ...` as shown. Without it, `COPY` writes root-owned + files that `vp install` cannot update (permission denied). - **Native addons**: the image includes a C/C++ build toolchain (`build-essential`, `python3`), so native dependencies such as `better-sqlite3` compile during `vp install`. diff --git a/rfcs/docker-image.md b/rfcs/docker-image.md index 587df37b55..f136cf6e5b 100644 --- a/rfcs/docker-image.md +++ b/rfcs/docker-image.md @@ -200,7 +200,11 @@ deployed images small. version on first use. This keeps the toolchain image ~190 MB smaller, more than a switch to Alpine/musl would save (and without the musl tradeoffs). - **User:** create a non-root `vp` user (mirroring Bun's `USER bun` and Deno's - `USER deno`); document switching to root for steps that need `apt`. + `USER deno`); document switching to root for steps that need `apt`. Because the + image runs as non-root, the documented multi-stage examples copy sources with + `COPY --chown=vp:vp ...`; without it `COPY` writes root-owned files that + `vp install` cannot update (permission denied). Verified end to end against the + published preview image. - **Alpine/musl variant (opt-in):** an `alpine:3` (musl) toolchain image (`docker/Dockerfile.alpine`), published under `-alpine` tags. It produces the smallest runtime (an Alpine SSR runtime measured ~136 MB vs ~150 MB distroless @@ -297,12 +301,14 @@ assert the SSR runtime Node.js matches the pinned `.node-version`): FROM ghcr.io/voidzero-dev/vite-plus:1 AS build WORKDIR /app -# Dependency layer first for cache reuse. -COPY package.json pnpm-lock.yaml .node-version ./ +# Dependency layer first for cache reuse. --chown is required: the image runs as +# the non-root vp user, and COPY would otherwise write root-owned files that +# vp install cannot update. +COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile # Build. vp reads .node-version and provisions that exact Node automatically. -COPY . . +COPY --chown=vp:vp . . RUN vp build # Export the exact resolved Node binary for the runtime stage. @@ -312,7 +318,7 @@ RUN cp "$(vp env which node | head -1)" /tmp/node # excluded; running --prod over the full install above would not prune them) --- FROM ghcr.io/voidzero-dev/vite-plus:1 AS deps WORKDIR /app -COPY package.json pnpm-lock.yaml .node-version ./ +COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile --prod # --- runtime stage: small, glibc, no vp --- @@ -346,9 +352,9 @@ not need a shell at runtime (see Future Work). ```dockerfile FROM ghcr.io/voidzero-dev/vite-plus:1 AS build WORKDIR /app -COPY package.json pnpm-lock.yaml .node-version ./ +COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile -COPY . . +COPY --chown=vp:vp . . RUN vp build FROM nginx:alpine AS runtime From 8bc91aab944dacc384f1aed8676090501335111f Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 21:27:14 +0800 Subject: [PATCH 14/18] ci(docker): post a sticky PR comment with preview image tags After both preview variants publish, post (or update) a single PR comment with the pr- / pr--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. --- .github/workflows/publish-to-pkg.pr.new.yml | 62 +++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/.github/workflows/publish-to-pkg.pr.new.yml b/.github/workflows/publish-to-pkg.pr.new.yml index e2960e8589..5133b8a8c4 100644 --- a/.github/workflows/publish-to-pkg.pr.new.yml +++ b/.github/workflows/publish-to-pkg.pr.new.yml @@ -201,3 +201,65 @@ jobs: echo "docker pull ${IMAGE}:${TAG}" echo '```' } >> "$GITHUB_STEP_SUMMARY" + + # Post (or update) a single sticky PR comment with the preview image tags after + # both variants publish successfully. Re-runs reuse the same comment via the + # hidden marker instead of creating a new one. + comment-docker-preview: + if: >- + github.repository == 'voidzero-dev/vite-plus' && + contains(github.event.pull_request.labels.*.name, 'pkg.pr.new') + name: Comment Docker preview + runs-on: ubuntu-latest + needs: publish-docker-preview + permissions: + pull-requests: write + steps: + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + IMAGE: ghcr.io/voidzero-dev/vite-plus + with: + script: | + const image = process.env.IMAGE; + const pr = context.payload.pull_request.number; + const marker = ''; + const body = [ + marker, + '## 🐳 Docker preview images', + '', + "Built from this PR's pkg.pr.new build:", + '', + '```bash', + `docker pull ${image}:pr-${pr}`, + `docker pull ${image}:pr-${pr}-alpine`, + '```', + '', + 'Quick check:', + '', + '```bash', + `docker run --rm ${image}:pr-${pr} vp --version`, + '```', + '', + 'See [docs/guide/docker.md](https://github.com/voidzero-dev/vite-plus/blob/main/docs/guide/docker.md) for usage.', + ].join('\n'); + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr, + }); + const existing = comments.find((c) => c.body && c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr, + body, + }); + } From 74b6dc97cd8306d1f5000564ffcbf62a853179e4 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 21:46:55 +0800 Subject: [PATCH 15/18] chore(docker): clarify comment-body form and add Dockerfile sync notes 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. --- .github/workflows/publish-to-pkg.pr.new.yml | 2 ++ docker/Dockerfile | 2 ++ docker/Dockerfile.alpine | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/publish-to-pkg.pr.new.yml b/.github/workflows/publish-to-pkg.pr.new.yml index 5133b8a8c4..df50e8db4c 100644 --- a/.github/workflows/publish-to-pkg.pr.new.yml +++ b/.github/workflows/publish-to-pkg.pr.new.yml @@ -223,6 +223,8 @@ jobs: const image = process.env.IMAGE; const pr = context.payload.pull_request.number; const marker = ''; + // Built as a line array (not a template literal) so the fenced code + // blocks don't collide with the YAML block-scalar indentation. const body = [ marker, '## 🐳 Docker preview images', diff --git a/docker/Dockerfile b/docker/Dockerfile index 146743c6e1..f2cf3a25ef 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -53,6 +53,8 @@ ENV VP_HOME=/home/vp/.vite-plus \ # downloads its own pinned Node at build time, so the default is dead weight in a # builder image. The node/npm/npx shims remain and fetch the right version on # first use. +# +# Keep this install line in sync with docker/Dockerfile.alpine. RUN curl -fsSL https://vite.plus | VP_VERSION="${VP_VERSION}" VP_PR_VERSION="${VP_PR_VERSION}" bash \ && vp --version \ && rm -rf "$VP_HOME/js_runtime" diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index f808d0dc92..7fc3420b3d 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -46,6 +46,8 @@ ENV VP_HOME=/home/vp/.vite-plus \ # Node.js: each project provisions its own pinned Node at build time, so the # default is dead weight. The node/npm/npx shims remain and fetch the right # version on first use. +# +# Keep this install line in sync with docker/Dockerfile. RUN curl -fsSL https://vite.plus | VP_VERSION="${VP_VERSION}" VP_PR_VERSION="${VP_PR_VERSION}" bash \ && vp --version \ && rm -rf "$VP_HOME/js_runtime" From 901e9726ee0006527468bf659b00b24177ae8fda Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 21:55:44 +0800 Subject: [PATCH 16/18] ci(docker): show preview image sizes in the sticky PR comment 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. --- .github/workflows/publish-to-pkg.pr.new.yml | 32 ++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-to-pkg.pr.new.yml b/.github/workflows/publish-to-pkg.pr.new.yml index df50e8db4c..d9b4e94c19 100644 --- a/.github/workflows/publish-to-pkg.pr.new.yml +++ b/.github/workflows/publish-to-pkg.pr.new.yml @@ -213,11 +213,36 @@ jobs: runs-on: ubuntu-latest needs: publish-docker-preview permissions: + contents: read + packages: read pull-requests: write + env: + IMAGE: ghcr.io/voidzero-dev/vite-plus + PR_NUMBER: ${{ github.event.pull_request.number }} steps: + - name: Log in to GHCR + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Measure image sizes + id: sizes + run: | + size() { + docker pull -q "$1" >/dev/null + numfmt --to=si --suffix=B "$(docker image inspect "$1" --format '{{.Size}}')" + } + { + echo "debian=$(size "${IMAGE}:pr-${PR_NUMBER}")" + echo "alpine=$(size "${IMAGE}:pr-${PR_NUMBER}-alpine")" + } >> "$GITHUB_OUTPUT" + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 env: - IMAGE: ghcr.io/voidzero-dev/vite-plus + DEBIAN_SIZE: ${{ steps.sizes.outputs.debian }} + ALPINE_SIZE: ${{ steps.sizes.outputs.alpine }} with: script: | const image = process.env.IMAGE; @@ -231,6 +256,11 @@ jobs: '', "Built from this PR's pkg.pr.new build:", '', + '| Image | Size |', + '| --- | --- |', + `| \`${image}:pr-${pr}\` | ${process.env.DEBIAN_SIZE} |`, + `| \`${image}:pr-${pr}-alpine\` | ${process.env.ALPINE_SIZE} |`, + '', '```bash', `docker pull ${image}:pr-${pr}`, `docker pull ${image}:pr-${pr}-alpine`, From 5bfb671cc7920a580f3b47ce53c38240c9e7e1e6 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 23:29:23 +0800 Subject: [PATCH 17/18] docs(docker): use a text link for the GitHub package page --- docs/guide/docker.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/guide/docker.md b/docs/guide/docker.md index 249849f1b1..876a5d7a0b 100644 --- a/docs/guide/docker.md +++ b/docs/guide/docker.md @@ -32,8 +32,7 @@ Tags track the `vp` version: Pin an exact tag (or a digest) for reproducible builds. The image is published for `linux/amd64` and `linux/arm64` and runs as a non-root user by default. -Browse all published versions and digests on the GitHub package page: -. +Browse all published versions and digests on the [GitHub package page](https://github.com/voidzero-dev/vite-plus/pkgs/container/vite-plus). The default image is Debian (glibc). An Alpine (musl) variant is published under the same versions with an `-alpine` suffix (`:latest-alpine`, `:0-alpine`, From d36ba17fad9db4c1a59e4fd66df070ffef98930e Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 25 Jun 2026 23:41:58 +0800 Subject: [PATCH 18/18] docs(docker): use :latest in examples instead of pinned versions 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. --- docs/guide/docker.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/guide/docker.md b/docs/guide/docker.md index 876a5d7a0b..a947c3c0f6 100644 --- a/docs/guide/docker.md +++ b/docs/guide/docker.md @@ -22,12 +22,12 @@ matching your project's Node version exactly. Tags track the `vp` version: -| Tag | Meaning | -| --------------------------------------- | ------------------------------------ | -| `ghcr.io/voidzero-dev/vite-plus:latest` | Latest release | -| `ghcr.io/voidzero-dev/vite-plus:0` | Latest 0.x | -| `ghcr.io/voidzero-dev/vite-plus:0.2` | Latest 0.2.x | -| `ghcr.io/voidzero-dev/vite-plus:0.2.2` | Exact version (first Docker release) | +| Tag | Meaning | +| -------------------------------------------------------- | -------------- | +| `ghcr.io/voidzero-dev/vite-plus:latest` | Latest release | +| `ghcr.io/voidzero-dev/vite-plus:` | Latest major | +| `ghcr.io/voidzero-dev/vite-plus:.` | Latest minor | +| `ghcr.io/voidzero-dev/vite-plus:..` | Exact version | Pin an exact tag (or a digest) for reproducible builds. The image is published for `linux/amd64` and `linux/arm64` and runs as a non-root user by default. @@ -35,8 +35,8 @@ for `linux/amd64` and `linux/arm64` and runs as a non-root user by default. Browse all published versions and digests on the [GitHub package page](https://github.com/voidzero-dev/vite-plus/pkgs/container/vite-plus). The default image is Debian (glibc). An Alpine (musl) variant is published under -the same versions with an `-alpine` suffix (`:latest-alpine`, `:0-alpine`, -`:0.2-alpine`, `:0.2.2-alpine`). See [Alpine variant](#alpine-musl-variant) for +the same versions with an `-alpine` suffix (`:latest-alpine`, +`:-alpine`, and so on). See [Alpine variant](#alpine-musl-variant) for when to use it and its tradeoffs. ## Production: SSR / Node-server app @@ -49,7 +49,7 @@ and the built app into a slim runtime stage: # syntax=docker/dockerfile:1 # --- build stage: the official Vite+ toolchain image --- -FROM ghcr.io/voidzero-dev/vite-plus:0.2.2 AS build +FROM ghcr.io/voidzero-dev/vite-plus:latest AS build WORKDIR /app # Install dependencies first so this layer is cached across source changes. @@ -67,7 +67,7 @@ RUN cp "$(vp env which node | head -1)" /tmp/node # A separate, fresh `--prod` install so devDependencies (including the vite-plus # toolchain) are excluded. Running `--prod` over the full install above would not # prune the already-installed devDependencies. -FROM ghcr.io/voidzero-dev/vite-plus:0.2.2 AS deps +FROM ghcr.io/voidzero-dev/vite-plus:latest AS deps WORKDIR /app COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile --prod @@ -113,7 +113,7 @@ A static site needs no Node.js at runtime; serve the build output with any stati server: ```dockerfile [Dockerfile] -FROM ghcr.io/voidzero-dev/vite-plus:0.2.2 AS build +FROM ghcr.io/voidzero-dev/vite-plus:latest AS build WORKDIR /app COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile @@ -131,7 +131,7 @@ Jenkins, and others): ```yaml [.gitlab-ci.yml] build: - image: ghcr.io/voidzero-dev/vite-plus:0.2.2 + image: ghcr.io/voidzero-dev/vite-plus:latest script: - vp install --frozen-lockfile - vp check @@ -148,7 +148,7 @@ preinstalled: ```jsonc [.devcontainer/devcontainer.json] { - "image": "ghcr.io/voidzero-dev/vite-plus:0.2.2", + "image": "ghcr.io/voidzero-dev/vite-plus:latest", } ``` @@ -178,7 +178,7 @@ pattern with an Alpine runtime: ```dockerfile [Dockerfile] # syntax=docker/dockerfile:1 -FROM ghcr.io/voidzero-dev/vite-plus:0.2.2-alpine AS build +FROM ghcr.io/voidzero-dev/vite-plus:latest-alpine AS build WORKDIR /app COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile @@ -186,7 +186,7 @@ COPY --chown=vp:vp . . RUN vp build RUN cp "$(vp env which node | head -1)" /tmp/node -FROM ghcr.io/voidzero-dev/vite-plus:0.2.2-alpine AS deps +FROM ghcr.io/voidzero-dev/vite-plus:latest-alpine AS deps WORKDIR /app COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./ RUN vp install --frozen-lockfile --prod @@ -206,7 +206,7 @@ CMD ["node", "dist/server.js"] ``` For a static SPA there is no Node.js at runtime, so only the builder changes: -swap the build stage to `ghcr.io/voidzero-dev/vite-plus:0.2.2-alpine`; the +swap the build stage to `ghcr.io/voidzero-dev/vite-plus:latest-alpine`; the `nginx:alpine` runtime and its output are unchanged. ## Notes