Skip to content

feat(docker): official Vite+ toolchain image#1944

Draft
fengmk2 wants to merge 18 commits into
mainfrom
docker-image
Draft

feat(docker): official Vite+ toolchain image#1944
fengmk2 wants to merge 18 commits into
mainfrom
docker-image

Conversation

@fengmk2

@fengmk2 fengmk2 commented Jun 25, 2026

Copy link
Copy Markdown
Member

Implements the official Vite+ Docker image from the RFC (rfcs/docker-image.md).

vp already provisions the exact Node.js from .node-version, so one toolchain image builds any project (no Node-version-keyed tags). It is not a production runtime image: a documented multi-stage build copies the resolved Node.js into a small, vp-free runtime stage.

Changes

  • docker/Dockerfile: glibc (debian:bookworm-slim) image bundling vp + native build toolchain, non-root user.
  • release.yml: publish-docker job, multi-arch (amd64/arm64) push to ghcr.io/voidzero-dev/vite-plus after npm publish, version-tagged.
  • docs/guide/docker.md + sidebar: multi-stage runtime, static SPA, CI, devcontainer, ad-hoc usage.

Verified locally: .node-version=24.15.0 resolves and installs exactly v24.15.0; the copied Node runs standalone in a plain debian:bookworm-slim stage.

Note: the first publish needs the GHCR package made public / linked to the repo in org settings.

Closes #1490

@fengmk2 fengmk2 self-assigned this Jun 25, 2026
@netlify

netlify Bot commented Jun 25, 2026

Copy link
Copy Markdown

Deploy Preview for viteplus-preview ready!

Name Link
🔨 Latest commit d36ba17
🔍 Latest deploy log https://app.netlify.com/projects/viteplus-preview/deploys/6a3d4c4f8f90770008f3cac6
😎 Deploy Preview https://deploy-preview-1944--viteplus-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

✅ Staging deployment successful!

Preview: https://viteplus-staging.void.app/
Commit: d36ba17

@pkg-pr-new

pkg-pr-new Bot commented Jun 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

vite-plus

npm i https://pkg.pr.new/voidzero-dev/vite-plus@1944

@voidzero-dev/vite-plus-core

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-core@1944

@voidzero-dev/vite-plus-prompts

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-prompts@1944

@voidzero-dev/vite-plus-cli-darwin-arm64

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-cli-darwin-arm64@1944

@voidzero-dev/vite-plus-cli-darwin-x64

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-cli-darwin-x64@1944

@voidzero-dev/vite-plus-cli-linux-arm64-gnu

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-cli-linux-arm64-gnu@1944

@voidzero-dev/vite-plus-cli-linux-arm64-musl

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-cli-linux-arm64-musl@1944

@voidzero-dev/vite-plus-cli-linux-x64-gnu

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-cli-linux-x64-gnu@1944

@voidzero-dev/vite-plus-cli-linux-x64-musl

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-cli-linux-x64-musl@1944

@voidzero-dev/vite-plus-cli-win32-arm64-msvc

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-cli-win32-arm64-msvc@1944

@voidzero-dev/vite-plus-cli-win32-x64-msvc

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-cli-win32-x64-msvc@1944

@voidzero-dev/vite-plus-darwin-arm64

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-darwin-arm64@1944

@voidzero-dev/vite-plus-darwin-x64

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-darwin-x64@1944

@voidzero-dev/vite-plus-linux-arm64-gnu

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-linux-arm64-gnu@1944

@voidzero-dev/vite-plus-linux-arm64-musl

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-linux-arm64-musl@1944

@voidzero-dev/vite-plus-linux-x64-gnu

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-linux-x64-gnu@1944

@voidzero-dev/vite-plus-linux-x64-musl

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-linux-x64-musl@1944

@voidzero-dev/vite-plus-win32-arm64-msvc

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-win32-arm64-msvc@1944

@voidzero-dev/vite-plus-win32-x64-msvc

npm i https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-win32-x64-msvc@1944

commit: 5bfb671

@fengmk2

fengmk2 commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

Manual verification commands for the Docker preview images

Preview images on GHCR (from the pkg.pr.new build):

  • ghcr.io/voidzero-dev/vite-plus:pr-1944 (Debian)
  • ghcr.io/voidzero-dev/vite-plus:pr-1944-alpine (Alpine/musl)

1. Basic functionality

docker run --rm ghcr.io/voidzero-dev/vite-plus:pr-1944 vp --version
docker run --rm ghcr.io/voidzero-dev/vite-plus:pr-1944 sh -c \
  'id -un; command -v vp; vp help >/dev/null && echo "vp help ok"; git --version'

2. Common vp commands (install / build / fmt / lint / test / check)

docker run --rm ghcr.io/voidzero-dev/vite-plus:pr-1944 bash -s <<'EOF'
set -e
cd /app
echo "24.15.0" > .node-version
printf 'node_modules/\ndist/\n' > .gitignore
cat > package.json <<'JSON'
{ "name": "vp-verify", "private": true, "type": "module", "devDependencies": { "vite-plus": "0.2.1" } }
JSON
mkdir -p src
cat > vite.config.ts <<'TS'
import { defineConfig } from 'vite-plus'
export default defineConfig({
  build: { ssr: 'src/server.ts', outDir: 'dist', rollupOptions: { output: { entryFileNames: 'server.js' } } },
})
TS
cat > src/greeting.ts <<'TS'
export function greeting(name: string): string {
  return `hello, ${name}`;
}
TS
cat > src/server.ts <<'TS'
import { createServer } from 'node:http';
import { greeting } from './greeting';
createServer((_req, res) => res.end(greeting('world'))).listen(3000);
TS
cat > src/greeting.test.ts <<'TS'
import { expect, test } from 'vitest';
import { greeting } from './greeting';
test('greeting', () => { expect(greeting('vp')).toBe('hello, vp'); });
TS
vp install --no-frozen-lockfile
node --version          # -> v24.15.0 (from .node-version)
vp build
vp fmt
vp lint
vp test
vp check
EOF

Swap the tag to ghcr.io/voidzero-dev/vite-plus:pr-1944-alpine to verify the Alpine image (same results, musl Node).

3. End-to-end multi-stage deploy (build app, run on a slim runtime)

mkdir -p vp-docker-e2e/src && cd vp-docker-e2e
echo "24.15.0" > .node-version
printf 'node_modules/\ndist/\n' > .gitignore
cat > package.json <<'JSON'
{ "name": "e2e", "private": true, "type": "module", "devDependencies": { "vite-plus": "0.2.1" } }
JSON
cat > vite.config.ts <<'TS'
import { defineConfig } from 'vite-plus'
export default defineConfig({
  build: { ssr: 'src/server.ts', outDir: 'dist', rollupOptions: { output: { entryFileNames: 'server.js' } } },
})
TS
cat > src/server.ts <<'TS'
import { createServer } from 'node:http'
createServer((_req, res) => { res.writeHead(200); res.end('vite-plus docker OK\n') })
  .listen(Number(process.env.PORT) || 3000)
TS
cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM ghcr.io/voidzero-dev/vite-plus:pr-1944 AS build
WORKDIR /app
COPY --chown=vp:vp package.json .node-version ./
RUN vp install --no-frozen-lockfile
COPY --chown=vp:vp . .
RUN vp build
RUN cp "$(vp env which node | head -1)" /tmp/node

FROM ghcr.io/voidzero-dev/vite-plus:pr-1944 AS deps
WORKDIR /app
COPY --chown=vp:vp package.json .node-version ./
RUN vp install --no-frozen-lockfile --prod

FROM debian:bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
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"]
EOF

docker build -t vp-e2e .
cid=$(docker run -d -p 3000:3000 vp-e2e)
sleep 2 && curl -s localhost:3000          # -> vite-plus docker OK
docker exec "$cid" /usr/local/bin/node --version   # -> v24.15.0
docker rm -f "$cid"

Note COPY --chown=vp:vp in the build/deps stages: the image runs as the non-root vp user, so without it vp install hits Permission denied. For an Alpine end-to-end build, use the -alpine tag and an alpine runtime stage (a musl Node only runs on a musl base); see docs/guide/docker.md.

fengmk2 added 14 commits June 25, 2026 21:30
Propose an official Vite+ toolchain Docker image on GHCR that bundles the
vp CLI for the build/CI/dev phases, plus a documented multi-stage pattern
that copies the exact .node-version Node into a slim glibc runtime (no vp),
keeping deployed images small while honoring the project's pinned Node.

Refs #1490, #1324
The image installs vp from npm via the official install script (pinned
VP_VERSION) and publishes after the npm release, rather than copying release
artifacts. Mark the RFC accepted with implementation in progress.
Add docker/Dockerfile for the official Vite+ toolchain image: a glibc
(debian:bookworm-slim) image that bundles the vp CLI for the build, CI, and
development phases. vp provisions the exact Node.js from .node-version at build
time, so the image is version-agnostic and needs no Node-keyed tags.

Add a publish-docker job to release.yml that builds the multi-arch
(amd64/arm64) image and pushes it to ghcr.io/voidzero-dev/vite-plus, tagged by
vp version, after the npm release is published.

Add docs/guide/docker.md documenting the recommended multi-stage pattern that
copies the resolved Node.js into a small, vp-free production runtime image,
plus static-SPA, CI, devcontainer, and ad-hoc usage.

Refs #1490
Add a publish-docker-preview job to publish-to-pkg.pr.new.yml that builds the
multi-arch image from the PR's pkg.pr.new build (VP_PR_VERSION) and pushes it as
ghcr.io/voidzero-dev/vite-plus:pr-<number>, so the image can be verified before
a real release.

Teach docker/Dockerfile an optional VP_PR_VERSION build arg, which installs vp
from pkg.pr.new instead of npm.

Refs #1490
Fixes vp check formatting failures in docs/guide/docker.md and
rfcs/docker-image.md (table alignment and emphasis markers).
- Drop xz-utils: vp only extracts .tar.gz (gzip), never xz.
- Drop redundant `mkdir -p /app && chown`: WORKDIR /app under USER vp already
  creates it owned by vp (verified).
- Combine the two ENV instructions into one layer.
- Build the per-PR preview image for linux/amd64 only; arm64 is covered by the
  release build and the test-install-sh-arm64 job, avoiding the slow QEMU leg
  on every labeled PR.
Reference why-reproductions-are-required/vite-plus-docker-example, which
CI-verifies the documented Dockerfile patterns end to end.
Running vp install --prod after a full vp install does not prune the
already-installed devDependencies (the large vite-plus toolchain), so the docs
pattern shipped ~164MB of dev toolchain into the runtime via COPY node_modules.
Install production dependencies in a dedicated deps stage (fresh --prod) instead,
and note that self-contained bundles can skip node_modules entirely. Also fix
the runtime size claim (smaller than the default node:* image, not -slim).
The installer pre-provisions a default Node.js (~190MB), but each project
provisions its own pinned Node at build time, so the default is dead weight in a
builder image. Remove it (rm -rf $VP_HOME/js_runtime) in the install layer; the
node/npm/npx shims remain and fetch the right version on first use. Toolchain
image: ~1.04GB -> ~846MB, more than an Alpine switch would save and without the
musl tradeoffs.
Publish an opt-in Alpine variant under -alpine tags (docker/Dockerfile.alpine),
built via a debian+alpine matrix in both the release and pkg.pr.new preview
workflows. It yields the smallest runtime (Alpine SSR ~136MB vs ~150MB
distroless, ~198MB debian-slim) for teams that standardize on Alpine.

Document the musl tradeoffs loudly: Node comes from the unofficial, unsigned
musl builds; native addons may need musl prebuilds or source compilation; and a
musl Node binary only runs on a musl base, so the runtime stage must also be
Alpine. The Debian image stays the recommended default.
Switch the example tags from the fictional :1 to the real 0.x scheme (:0, :0.2,
:0.2.2 and -alpine variants), since 0.2.2 is the first published image. Add a
link to the GitHub package page to browse all published versions and digests.
The image runs as the non-root vp user, so COPY without --chown writes
root-owned files that vp install cannot update (permission denied) when it needs
to write package.json or the lockfile (e.g. no committed lockfile, or vp add).
Use COPY --chown=vp:vp in the build/deps stages. Verified end to end against the
published pr-1944 preview image.
After both preview variants publish, post (or update) a single PR comment with
the pr-<n> / pr-<n>-alpine docker pull commands. Uses a hidden marker so re-runs
reuse the same comment instead of creating new ones. Implemented with the
existing actions/github-script (no new dependency); needs pull-requests: write.
Comment thread .github/workflows/publish-to-pkg.pr.new.yml
fengmk2 added 2 commits June 25, 2026 21:46
Document why the sticky-comment body uses a line array (YAML block-scalar vs
fenced code blocks) and add 'keep in sync' notes on the install RUN line shared
verbatim between docker/Dockerfile and docker/Dockerfile.alpine.
Measure each published preview image (docker pull + image inspect) and render a
size table in the comment, alongside the pull commands.

Addresses review feedback on #1944.
@github-actions

Copy link
Copy Markdown
Contributor

🐳 Docker preview images

Built from this PR's pkg.pr.new build:

Image Size
ghcr.io/voidzero-dev/vite-plus:pr-1944 923MB
ghcr.io/voidzero-dev/vite-plus:pr-1944-alpine 786MB
docker pull ghcr.io/voidzero-dev/vite-plus:pr-1944
docker pull ghcr.io/voidzero-dev/vite-plus:pr-1944-alpine

Quick check:

docker run --rm ghcr.io/voidzero-dev/vite-plus:pr-1944 vp --version

See docs/guide/docker.md for usage.

@fengmk2

fengmk2 commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

An exception was found that may be related to the vite task. The lint command runs normally when executed directly, but throws an error after passing through the task runner.

docker run --rm ghcr.io/voidzero-dev/vite-plus:pr-1944 bash -c \
  'vp create vite:monorepo --directory hello --no-interactive && cd hello && vp lint && vp run ready'

◇ Scaffolded hello with Vite+ monorepo
• Node 24.18.0  pnpm 11.9.0
✓ Dependencies installed in 27s
→ Next: cd hello && vp run
Found 0 warnings and 0 errors.
Finished in 1.2s on 6 files with 111 rules using 16 threads.
$ vp check
pass: All 18 files are correctly formatted (1112ms, 16 threads)
Error running tsgolint: "exit status: exit status: 1"/app/hello/node_modules/.pnpm/oxlint-tsgolint@0.23.0/node_modules/oxlint-tsgolint/bin/tsgolint.js:18
    throw e;
    ^

<ref *1> Error: spawnSync /app/hello/node_modules/.pnpm/@oxlint-tsgolint+linux-x64@0.23.0/node_modules/@oxlint-tsgolint/linux-x64/tsgolint EINVAL
    at Object.spawnSync (node:internal/child_process:1143:20)
    at spawnSync (node:child_process:911:24)
    at Object.execFileSync (node:child_process:954:15)
    at Object.<anonymous> (/app/hello/node_modules/.pnpm/oxlint-tsgolint@0.23.0/node_modules/oxlint-tsgolint/bin/tsgolint.js:11:17)
    at Module._compile (node:internal/modules/cjs/loader:1871:14)
    at Object..js (node:internal/modules/cjs/loader:2002:10)
    at Module.load (node:internal/modules/cjs/loader:1594:32)
    at Module._load (node:internal/modules/cjs/loader:1396:12)
    at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
    at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5) {
  errno: -22,
  code: 'EINVAL',
  syscall: 'spawnSync /app/hello/node_modules/.pnpm/@oxlint-tsgolint+linux-x64@0.23.0/node_modules/@oxlint-tsgolint/linux-x64/tsgolint',
  path: '/app/hello/node_modules/.pnpm/@oxlint-tsgolint+linux-x64@0.23.0/node_modules/@oxlint-tsgolint/linux-x64/tsgolint',
  spawnargs: [ 'headless' ],
  error: [Circular *1],
  status: null,
  signal: null,
  output: null,
  pid: 0,
  stdout: undefined,
  stderr: undefined
}

Node.js v24.18.0

Linting failed before analysis started
error: Linting could not start

cc @wan9chi

Avoid hardcoded version tags in the runnable examples so users do not copy an
outdated pin; the tags table now documents the scheme with placeholders.
@fengmk2

fengmk2 commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d36ba17fad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/guide/docker.md
WORKDIR /app

# Install dependencies first so this layer is cached across source changes.
COPY --chown=vp:vp package.json pnpm-lock.yaml .node-version ./

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not require .node-version in the Docker examples

This example makes .node-version a mandatory Docker COPY source even though the guide says projects can pin Node via engines.node or devEngines.runtime, and the resolver does support those fallbacks. For projects that rely only on package.json, Docker fails at this COPY step before vp can read the package metadata; the same required source is repeated in the deps/static/Alpine examples. Please either make the baseline examples copy only files that always exist or show .node-version as an optional variant.

Useful? React with 👍 / 👎.

Comment thread docker/Dockerfile
&& vp --version \
&& rm -rf "$VP_HOME/js_runtime"

WORKDIR /app

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Create /app with ownership for the vp user

When downstream Dockerfiles follow the new guide and run WORKDIR /app plus RUN vp install as the default vp user, the inherited /app directory from this image is root-owned because Docker creates missing WORKDIR directories as root even after USER. COPY --chown=vp:vp ... fixes the files but not the directory itself, so vp install cannot create node_modules under /app; the Alpine image has the same pattern. Please create/chown /app before switching users or set the workdir somewhere already writable by vp.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: official Docker image that honors .node-version

1 participant