| | |

Vite 17 ⚡ Preview Server — Testing production builds locally

⏱️ Estimated read: 15 min | |

The vite preview command exists to answer one question: does the build in dist/ actually work when served the way a static host would serve it? The development server answers a different question—does the source code work with HMR, module transforms, and the dev-time resolution algorithm. Those are not the same thing. A build can pass the dev server and fail in production because a dependency was externalized incorrectly, a dynamic import was transformed differently, or an asset path resolved against the wrong base URL. The preview server closes that gap by serving the built output without the dev-time machinery, but it is not a production server and does not pretend to be.

The Vite documentation is explicit about the scope: “The vite preview command will boot up a local static web server that serves the files from dist at http://localhost:4173. It’s an easy way to check if the production build looks OK in your local environment” . The same source adds a warning that the preview command is “not meant to be used in production” . This distinction matters because the preview server lacks the caching headers, compression, TLS termination, and process supervision that a real production deployment requires. It is a local verification tool, not a deployment target.

Key point: The preview server serves the contents of dist/ over a local HTTP server with a default port of 4173. It is configured through preview options in vite.config, most of which inherit from the server options but can be overridden independently. It is not designed for production use and should not be exposed to public traffic.


Why the preview server exists separately from the dev server

The transform mismatch problem. The development server transforms modules on the fly. It resolves bare imports through the resolve configuration, applies plugins in a specific order, and injects HMR code. The production build runs a different pipeline: it bundles, tree-shakes, and minifies. A module that works in development can fail in the build if a plugin behaves differently in the build phase, if a dependency was marked external incorrectly, or if a dynamic import resolved to a path that no longer exists after bundling. The preview server exercises the built output, not the source. It is the first place where those differences surface locally.

The base URL assumption. The dev server serves from /. A production build may be configured with a base path—/app/, for example—if it will be deployed under a subpath. The preview server respects the base configuration from the build, so asset URLs and routing behave the same way they will on the host. Testing against the dev server with a different base is not equivalent.

The preview options inheritance. The preview server’s configuration options default to the server options but can be overridden in the preview block . The port defaults to 4173 rather than the dev server’s 5173, which allows both to run simultaneously. The host, https, proxy, cors, and headers options all inherit from server unless explicitly overridden in preview. This design means a developer who configured a proxy for the dev server gets the same proxy in preview without duplicating the configuration, but can change the port or headers independently if the build behaves differently.

The trade-off. The preview server does not simulate a production CDN, an HTTP/2 server, or a reverse proxy with caching rules. It serves static files over a simple Node.js HTTP server. If the production environment depends on specific headers, compression, or routing rules that the preview server does not replicate, the preview test will pass and the production deploy will still fail. The preview server catches build-time and resolution-time errors, not infrastructure-level errors.


a. Configuring the preview server

The preview block in vite.config controls the server’s behavior. Every option inherits from its server counterpart unless overridden.

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  // The base path is applied to the build and respected by preview.
  // If the app will be deployed under /admin/, set it here.
  base: '/admin/',
  preview: {
    // The port defaults to 4173.
    // Override it if the default is occupied or conflicts with another tool.
    port: 8080,
    // strictPort: true means the server exits if the port is in use
    // instead of trying the next available port.
    // Useful in CI where a silently different port breaks the test runner.
    strictPort: true,
    // host: true listens on all addresses, including LAN.
    // Default is server.host.
    host: true,
    // open: true opens the browser automatically on start.
    open: true,
  },
});

The strictPort option is the one that matters most for automated testing. The default behavior tries the next available port if 4173 is occupied, which means a CI script that hardcodes http://localhost:4173 can silently connect to the wrong server or fail with a confusing error . Setting strictPort: true makes the failure immediate and obvious.

The base configuration deserves attention in the preview context. If base is set to a subpath, the preview server serves the build at that subpath. Visiting http://localhost:4173/ returns a 404; the correct URL is http://localhost:4173/admin/. This is not a bug—it is the same behavior the production host will exhibit if the build is deployed under that subpath. The preview server is verifying that the base configuration is correct, not abstracting it away.


b. Running the preview server and overriding options from the CLI

The preview server is invoked through the vite preview command, typically via a preview script in package.json. CLI flags can override the configuration file for one-off runs.

// package.json
{
  "scripts": {
    "build": "vite build",
    "preview": "vite preview"
  }
}
# Build first. The preview server serves whatever is in dist/.
npm run build

# Start the preview server with the default port (4173).
npm run preview

# Override the port from the CLI.
# This takes precedence over the preview.port in vite.config.
npm run preview -- --port 8080

# Listen on all addresses for testing from a phone or another machine.
npm run preview -- --host

The CLI flags mirror the configuration options: --port, --host, --strictPort, --open, --https . The -- separator is required when passing flags through npm run; without it, npm intercepts the flags instead of forwarding them to Vite.

The preview server does not watch for changes. It serves a snapshot of dist/ as it existed when the server started. A rebuild requires stopping the server and starting it again, or running vite build in one terminal and restarting the preview command in another. This is intentional—the preview server is testing a specific build artifact, not a live development environment.


c. What the preview server does not do

The preview server’s limitations are worth stating plainly because they are the source of false confidence. The preview server does not compress responses with gzip or Brotli, does not set cache headers, does not serve over HTTP/2, does not handle TLS unless explicitly configured, and does not apply the routing rules of a reverse proxy or CDN. It is a static file server with a few conveniences.

The preview.headers option allows setting response headers, which is useful for testing a Content-Security-Policy or a CORS configuration before deployment . The preview.proxy option allows proxying API requests to a backend, mirroring the development server’s proxy configuration . These options make the preview server a closer approximation of the production environment, but they do not make it equivalent to one.

The Vite documentation’s warning is unambiguous: the preview server is “not designed for production use” . It runs in a single Node.js process with no clustering, no graceful shutdown, and no error recovery. A production deployment requires a proper static host—a CDN, Nginx, Caddy, or a platform like Netlify or Vercel—that provides those guarantees.


Complete Example Session

// ============================================
// PART 1: THE BUILD CONFIGURATION
// ============================================

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  // The base path is applied to the build.
  // The preview server respects it.
  base: '/',
  build: {
    outDir: 'dist',
  },
  preview: {
    // Default port is 4173. Override to avoid conflicts.
    port: 4173,
    // Exit if the port is occupied instead of trying the next one.
    // Critical for CI where the expected URL must be reachable.
    strictPort: true,
    // Allow access from the local network for device testing.
    host: true,
    // Custom headers for testing CSP or CORS in preview.
    headers: {
      'X-Frame-Options': 'DENY',
    },
  },
});

// ============================================
// PART 2: THE BUILD AND PREVIEW SCRIPTS
// ============================================

// package.json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "preview:strict": "vite preview --strictPort --port 4173"
  }
}

// ============================================
// PART 3: A TYPICAL VERIFICATION WORKFLOW
// ============================================

// Step 1: Build the application.
// The output is written to dist/.
//   npm run build
//
// Step 2: Start the preview server.
// It serves dist/ at http://localhost:4173.
//   npm run preview
//
// Step 3: Open the browser and verify:
//   - The page renders without console errors.
//   - Asset URLs resolve (CSS, images, fonts).
//   - Dynamic imports load correctly.
//   - Client-side routing works (navigate between routes).
//   - API calls reach the expected endpoints (if proxy is configured).
//
// Step 4: Test the base path.
//   If base is '/admin/', visit http://localhost:4173/admin/.
//   Visiting the root should 404. This is correct.
//
// Step 5: Stop the server.
//   Ctrl+C in the terminal.

// ============================================
// PART 4: THE BASE PATH PITFALL
// ============================================

// vite.config.ts — with a subpath base
export default defineConfig({
  base: '/my-app/',
});

// After build and preview:
// http://localhost:4173/        → 404
// http://localhost:4173/my-app/ → the application
//
// This is not a bug. The build was configured for a subpath,
// and the preview server serves it at that subpath.
// If the root URL is expected to work, the base is wrong.

// ============================================
// PART 5: PROXYING API REQUESTS IN PREVIEW
// ============================================

// vite.config.ts
export default defineConfig({
  preview: {
    // The proxy inherits from server.proxy by default.
    // Override it here if the preview needs a different target.
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
      },
    },
  },
});

// With this configuration, a fetch to /api/users in the preview
// is forwarded to http://localhost:3000/api/users.
// This mirrors the development server behavior and allows
// testing the built client against a real backend.

// ============================================
// PART 6: THE PREVIEW SERVER IS NOT PRODUCTION
// ============================================

// The preview server:
// - Serves static files with no compression.
// - Does not cache responses.
// - Does not cluster or handle high concurrency.
// - Does not provide TLS unless configured.
// - Does not implement CDN routing or edge rules.
//
// It is a local verification tool.
// The production deployment requires a real static host.

// ============================================
// PART 7: CI VERIFICATION WITH PREVIEW
// ============================================

// A CI script that builds and verifies the preview:
//
// 1. npm ci
// 2. npm run build
// 3. npm run preview -- --port 4173 &
// 4. Wait for the server to respond on http://localhost:4173
// 5. Run a smoke test (curl or Playwright) against the URL
// 6. Kill the preview server
//
// The strictPort flag is essential here.
// Without it, the server might start on port 4174 and the
// smoke test against 4173 would fail with a connection refused
// instead of testing the application.

// ============================================
// PART 8: PREVIEW VS DEV — WHAT EACH CATCHES
// ============================================

// Dev server catches:
// - Syntax errors during transform.
// - Import resolution errors in the source.
// - HMR boundary issues.
// - TypeScript errors (if a checker is configured).
//
// Preview server catches:
// - Build-time tree-shaking errors.
// - Externalization mistakes (a bundled peer dependency).
// - Dynamic import path errors after bundling.
// - Base path configuration errors.
// - Asset URL resolution in the built output.
//
// Neither catches:
// - Production infrastructure issues (CDN, caching, TLS).
// - Concurrency and load behavior.
// - Environment variable differences between local and production.

// ============================================
// PART 9: PREVIEW WITH HTTPS
// ============================================

// vite.config.ts
import basicSsl from '@vitejs/plugin-basic-ssl';

export default defineConfig({
  plugins: [basicSsl()],
  preview: {
    // https inherits from server.https.
    // With the plugin, the preview server serves over HTTPS.
    // Useful for testing features that require a secure context
    // (camera, geolocation, service workers).
  },
});

// ============================================
// PART 10: VERIFICATION CHECKLIST
// ============================================

// [ ] Build completes without errors.
// [ ] Preview server starts on the expected port.
// [ ] Root URL (or base URL) renders the application.
// [ ] No 404s for assets in the network tab.
// [ ] Dynamic imports resolve.
// [ ] Client-side routing works on navigation.
// [ ] API proxy (if configured) reaches the backend.
// [ ] Base path is correct for the deployment target.
// [ ] The server is stopped after verification.

The ten parts covered the configuration, the build-preview workflow, the base path behavior, the proxy inheritance, the CI verification pattern, and the boundary between what the preview server tests and what it cannot test. The through-line is that the preview server verifies the build artifact, not the infrastructure.


Quick Reference

Preview options

OptionTypeDefaultPurpose
preview.hoststring | booleanserver.hostListen address
preview.portnumber4173Server port
preview.strictPortbooleanserver.strictPortExit if port is occupied
preview.httpshttps.ServerOptionsserver.httpsTLS + HTTP/2
preview.openboolean | stringserver.openOpen browser on start
preview.proxyRecordserver.proxyAPI proxy rules
preview.corsboolean | CorsOptionsserver.corsCORS configuration
preview.headersOutgoingHttpHeaders—Response headers

Source:

CLI flags

FlagEffect
--port <n>Override preview.port
--hostListen on all addresses
--strictPortExit if port is occupied
--openOpen browser
--httpsEnable HTTPS

What preview catches vs what it misses

CatchesMisses
Build-time resolution errorsCDN caching behavior
Externalization mistakesCompression (gzip/Brotli)
Base path configurationTLS termination
Asset URL resolutionConcurrency and clustering
Dynamic import pathsEnvironment variable differences

Best Practices

✅ Do This:

// Build before preview
npm run build && npm run preview                              // ✅

// Set strictPort for CI
preview: { strictPort: true }                                 // ✅

// Verify the base path matches the deployment target
base: '/my-app/', // visit /my-app/ in preview               // ✅

// Use preview.headers for CSP testing
preview: { headers: { 'X-Frame-Options': 'DENY' } }           // ✅

// Use the proxy for API testing in preview
preview: { proxy: { '/api': { target: 'http://localhost:3000' } } } // ✅

❌ Don’t Do This:

// Deploy the preview server to production
vite preview --host 0.0.0.0                                  // ❌

// Expect the preview server to watch for changes
// It serves a snapshot of dist/                               // ❌

// Ignore the base path when testing
// Visit http://localhost:4173/ when base is '/admin/'          // ❌ (404)

// Assume preview tests infrastructure
// Compression, caching, and TLS are not simulated            // ❌

Common Pitfalls

PitfallWhy It HappensFix
404 at the root URLbase is set to a subpathVisit the subpath or change base
Preview serves stale buildServer started before vite build completedRebuild and restart the preview server
Port conflict in CIstrictPort not set; server moved to a different portSet strictPort: true and use a fixed port
API calls fail in previewProxy not configured or target wrongConfigure preview.proxy or server.proxy
Assets missing after buildbase incorrect for the asset pathsVerify base matches the deployment URL

Real-World Examples

1. Basic build and preview

npm run build && npm run preview

2. Strict port for CI

preview: { strictPort: true, port: 4173 }

3. Base path

base: '/admin/';
// Visit http://localhost:4173/admin/

4. Proxy API

preview: { proxy: { '/api': 'http://localhost:3000' } }

5. Custom headers

preview: { headers: { 'X-Custom': 'value' } }

6. Open browser

preview: { open: true }

7. Listen on LAN

preview: { host: true }

8. HTTPS preview

plugins: [basicSsl()]; // https inherited from server

9. CLI port override

npm run preview -- --port 8080

10. CI smoke test

npm run preview -- --strictPort & curl http://localhost:4173

Visual

Build and Preview Workflow

┌──────────────────────────────────────────────────────────────┐
│  FROM SOURCE TO VERIFIED BUILD                               │
│                                                              │
│  Source files                                                │
│  ┌─────────────────────┐                                    │
│  │ src/                │                                    │
│  │ vite.config.ts      │                                    │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                │
│  ┌─────────────────────┐                                    │
│  │ npm run build       │  Bundles, minifies, tree-shakes    │
│  │ vite build          │  Applies base path                 │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                │
│  ┌─────────────────────┐                                    │
│  │ dist/               │  The build artifact                │
│  │ index.html          │  This is what will be deployed     │
│  │ assets/*.js         │                                    │
│  │ assets/*.css        │                                    │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                │
│  ┌─────────────────────┐                                    │
│  │ npm run preview     │  Serves dist/ at localhost:4173   │
│  │ vite preview        │  Respects base, proxy, headers    │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                │
│  ┌─────────────────────┐                                    │
│  │ Browser verification│  Does the build actually work?    │
│  └─────────────────────┘                                    │
│                                                              │
│  The preview server tests the artifact, not the source.     │
└──────────────────────────────────────────────────────────────┘

What Preview Inherits from Server

┌──────────────────────────────────────────────────────────────┐
│  CONFIGURATION INHERITANCE                                   │
│                                                              │
│  server: {                    preview: {                     │
│    host: 'localhost',  ────▶   host: 'localhost'  (inherited)│
│    port: 5173,         ────▶   port: 4173         (overridden)│
│    https: {},          ────▶   https: {}          (inherited)│
│    proxy: {},          ────▶   proxy: {}          (inherited)│
│    cors: true,         ────▶   cors: true         (inherited)│
│    headers: {},        ────▶   headers: {}        (inherited)│
│  }                            }                              │
│                                                              │
│  Override only what the build needs.                         │
│  The port is the most common override.                      │
│  The proxy is the second most common.                       │
└──────────────────────────────────────────────────────────────┘

The Base Path Behavior

┌──────────────────────────────────────────────────────────────┐
│  BASE: '/admin/'                                             │
│                                                              │
│  http://localhost:4173/                                      │
│  ┌─────────────────────┐                                    │
│  │ 404 Not Found       │  The root is not the app.          │
│  └─────────────────────┘                                    │
│                                                              │
│  http://localhost:4173/admin/                                │
│  ┌─────────────────────┐                                    │
│  │ The application     │  Assets resolve under /admin/.     │
│  │ assets load         │  Routing works.                    │
│  └─────────────────────┘                                    │
│                                                              │
│  This is the same behavior the production host will         │
│  exhibit. The preview server is verifying the base          │
│  configuration, not hiding it.                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Commandvite preview
Default port4173
Servesdist/ (the build output)
Configuration blockpreview in vite.config
InheritanceMost options inherit from server
Production useNot intended; local verification only
Port override--port CLI flag or preview.port
CI recommendationstrictPort: true with a fixed port

Key takeaways:

  • The preview server tests the build artifact, not the source. The dev server transforms on the fly; the preview server serves what vite build produced. A build that works in dev and fails in preview has a build-time problem—externalization, tree-shaking, or path resolution—that the dev server masks .
  • The default port is 4173, not 5173. The preview server is designed to run alongside the dev server. The port can be overridden via preview.port or the --port CLI flag .
  • strictPort: true is essential for CI. Without it, the server silently moves to the next available port if 4173 is occupied, and a CI script that hardcodes the URL connects to the wrong server or fails with a confusing error .
  • The base configuration is respected, not hidden. If base is /admin/, the preview server serves at /admin/, and the root URL returns 404. This is the correct behavior for verifying a subpath deployment.
  • The preview server is not a production server. It does not compress, cache, cluster, or provide TLS unless configured. It is a local static file server with conveniences. Deploying it to public traffic is a misconfiguration, not a deployment strategy .

Remember: The preview server exists to close the gap between “the build succeeded” and “the build works.” The build succeeding is a compiler verdict; the build working is a runtime verdict. The preview server provides the runtime environment—a static file server with the base path, proxy, and headers from the configuration—without the development server’s transformations. It is the last local check before deployment, and it catches exactly the class of errors that the dev server cannot: incorrect externalization, broken dynamic import paths after bundling, and base path misconfiguration. It does not catch infrastructure errors, because it is not infrastructure. It is a verification tool, and its value is proportional to how closely the verification matches the production environment it is approximating.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!