Skip to content

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

Open
fengmk2 wants to merge 27 commits into
mainfrom
docker-image
Open

feat(docker): official Vite+ toolchain image#1944
fengmk2 wants to merge 27 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 3030eee
🔍 Latest deploy log https://app.netlify.com/projects/viteplus-preview/deploys/6a3e38937f6ca206cf8b6e12
😎 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: 3030eee

@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: 91b8f02

@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.

Comment thread .github/workflows/publish-to-pkg.pr.new.yml
fengmk2 added a commit that referenced this pull request Jun 25, 2026
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

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

🐳 Docker preview images

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

Image Compressed size
ghcr.io/voidzero-dev/vite-plus:pr-1944 340MB
ghcr.io/voidzero-dev/vite-plus:pr-1944-alpine 291MB
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

@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 Outdated
Comment thread docker/Dockerfile
fengmk2 added a commit that referenced this pull request Jun 26, 2026
Copy only package.json + lockfile in the cached dependency layer so projects
that pin Node via engines.node / devEngines.runtime (no .node-version file) do
not fail at COPY. .node-version, when used, is picked up from the full COPY . .
before vp build.

Addresses review feedback on #1944.
@fengmk2

fengmk2 commented Jun 26, 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: 3a61144d72

ℹ️ 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 Outdated
Comment thread .github/workflows/release.yml
Comment thread docker/Dockerfile
Comment thread docker/Dockerfile
fengmk2 added 7 commits June 26, 2026 15:34
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.
fengmk2 added 13 commits June 26, 2026 15:34
The installer pre-provisions a default Node.js (~190MB), but each project
provisions its own pinned Node at build time, so the default is dead weight in a
builder image. Remove it (rm -rf $VP_HOME/js_runtime) in the install layer; the
node/npm/npx shims remain and fetch the right version on first use. Toolchain
image: ~1.04GB -> ~846MB, more than an Alpine switch would save and without the
musl tradeoffs.
Publish an opt-in Alpine variant under -alpine tags (docker/Dockerfile.alpine),
built via a debian+alpine matrix in both the release and pkg.pr.new preview
workflows. It yields the smallest runtime (Alpine SSR ~136MB vs ~150MB
distroless, ~198MB debian-slim) for teams that standardize on Alpine.

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

Addresses review feedback on #1944.
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.
Copy only package.json + lockfile in the cached dependency layer so projects
that pin Node via engines.node / devEngines.runtime (no .node-version file) do
not fail at COPY. .node-version, when used, is picked up from the full COPY . .
before vp build.

Addresses review feedback on #1944.
… summary

Read the compressed download size from the registry manifest (no docker pull,
~425MB saved per run; matches what users pull and the GHCR package page), and
remove the per-leg step summary now superseded by the sticky PR comment.
Use a .node-version* glob in the dependency COPY so the file is optional (no
failure for engines.node/devEngines.runtime projects) yet available in every
stage, including the SSR deps stage, when present. Reword the tag note so the
:latest examples and the 'pin for reproducibility' advice no longer conflict.
fengmk2 added 2 commits June 26, 2026 15:48
git is installed with --no-install-recommends, which omits the SSH client, so
git+ssh dependencies would fail during vp install; add openssh-client (apt and
apk). Also create + chown /app to the vp user so downstream WORKDIR /app +
vp install can write regardless of the builder.

Addresses review feedback on #1944.
publish-docker needs the whole Release job; its last step is the Discord
webhook. A notification failure must not fail Release (npm/GitHub release are
already published) or skip GHCR publishing. Mark the step continue-on-error.

Addresses review feedback on #1944.
@fengmk2

fengmk2 commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

@codex review

Move the Discord notification out of the Release job into a terminal
discord-notify job that runs after publish-docker, so the message can include
the GHCR image (docker pull + alpine tag). This also fully removes the earlier
concern about a notification failure affecting Docker publishing (Discord is now
downstream of it). Webhook secret is a repo secret (used by request-approval
without the release environment), so no environment/approval gate is needed.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 36989b2c29

ℹ️ 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 Outdated
fengmk2 added 2 commits June 26, 2026 16:18
publish-docker already needs Release, so listing it again is redundant; depend
on [check, publish-docker] (check is kept for its outputs).
Node.js is the trademarked product name (OpenJS Foundation); bare 'Node' is
incorrect. Use 'Node.js' for the runtime/version in prose and keep lowercase
'node' for the binary/command and image tags. Applied across the Docker guide,
the RFC, and the Dockerfile comments.

Addresses review feedback on #1944.
@fengmk2 fengmk2 marked this pull request as ready for review June 26, 2026 08:26
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