DEV Community

Cover image for Inertia.js + Vue/React on Deploynix: SSR Setup and Build Pipeline
Deploynix
Deploynix

Posted on • Originally published at deploynix.io

Inertia.js + Vue/React on Deploynix: SSR Setup and Build Pipeline

Inertia.js bridges the gap between server-side Laravel applications and modern JavaScript frontends. You get the productivity of Laravel routing, controllers, and middleware combined with the interactivity of Vue or React, without building a separate API. But deploying an Inertia application to production involves more than just pushing code. You need a build pipeline for your JavaScript assets, and if you want server-side rendering, you need a Node.js process running alongside your PHP application.

This guide covers the full deployment pipeline for Inertia.js applications on Deploynix, from configuring build scripts in deployment hooks through setting up SSR and troubleshooting the issues that commonly arise in production.

Understanding the Deployment Pipeline

An Inertia.js application has two build outputs:

  1. Client-side assets: JavaScript and CSS compiled by Vite, served to the browser. These go into your public/build directory.
  2. SSR bundle (optional): A Node.js-compatible JavaScript file that renders your pages on the server for the initial page load.

Both need to be built during deployment, and the SSR bundle needs a running Node.js process to serve requests.

Configuring Vite for Production

Laravel ships with Vite integration out of the box. Your vite.config.js should be configured for your framework of choice.

For Vue:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [
        laravel({
            input: 'resources/js/app.js',
            ssr: 'resources/js/ssr.js',
            refresh: true,
        }),
        vue({
            template: {
                transformAssetUrls: {
                    base: null,
                    includeAbsolute: false,
                },
            },
        }),
    ],
});
Enter fullscreen mode Exit fullscreen mode

For React:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [
        laravel({
            input: 'resources/js/app.jsx',
            ssr: 'resources/js/ssr.jsx',
            refresh: true,
        }),
        react(),
    ],
});
Enter fullscreen mode Exit fullscreen mode

The ssr key tells Vite where to find your SSR entry point. This file sets up the server-side rendering logic.

Setting Up Deployment Hooks

Deploynix supports deployment hooks that run custom commands at different stages of the deployment process. For Inertia applications, you need to build assets during deployment.

Build Script in Deployment Hooks

Configure a deployment hook that runs after your code is deployed but before the site goes live:

cd /home/deploynix/your-site

# Install Node.js dependencies
npm ci --production=false

# Build client-side assets
npm run build

# Build SSR bundle (if using SSR)
npm run build:ssr
Enter fullscreen mode Exit fullscreen mode

Why **npm ci instead of npm install?**

npm ci installs dependencies from your package-lock.json exactly as specified, ensuring reproducible builds. It's faster than npm install in CI/CD environments because it skips the dependency resolution step. The --production=false flag ensures devDependencies (like Vite, Vue compiler, etc.) are installed, which are needed for the build step.

Important: Make sure your package-lock.json is committed to your repository. Without it, npm ci will fail.

Optimizing Build Times

Asset builds can take 30-60 seconds or longer for large applications. Here are strategies to minimize deployment downtime:

Cache node_modules: If your Node.js dependencies haven't changed, you can skip npm ci. Check if node_modules exists and if package-lock.json has changed since the last install:

# In your deployment hook
if [ ! -d "node_modules" ] || [ "package-lock.json" -nt "node_modules/.package-lock.json" ]; then
    npm ci --production=false
fi

npm run build
Enter fullscreen mode Exit fullscreen mode

Use Deploynix's zero-downtime deployments: Deploynix supports zero-downtime deployments, meaning the build happens in a new release directory while the previous version continues serving traffic. The switchover happens only after the build completes successfully.

Setting Up Server-Side Rendering

SSR improves your application's initial page load performance and SEO. Without SSR, the browser receives an empty HTML shell and must download, parse, and execute JavaScript before rendering any content. With SSR, the server sends fully rendered HTML, and JavaScript hydrates it for interactivity.

The SSR Entry Point

Vue SSR entry point (resources/js/ssr.js):

import { createSSRApp, h } from 'vue';
import { renderToString } from 'vue/server-renderer';
import { createInertiaApp } from '@inertiajs/vue3';
import createServer from '@inertiajs/vue3/server';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { route } from '../../vendor/tightenco/ziggy';

createServer((page) =>
    createInertiaApp({
        page,
        render: renderToString,
        title: (title) => `${title} - Your App`,
        resolve: (name) =>
            resolvePageComponent(
                `./Pages/${name}.vue`,
                import.meta.glob('./Pages/**/*.vue'),
            ),
        setup({ App, props, plugin }) {
            return createSSRApp({ render: () => h(App, props) })
                .use(plugin)
                .mixin({
                    methods: {
                        route: (name, params, absolute) =>
                            route(name, params, absolute, page.props.ziggy),
                    },
                });
        },
    }),
);
Enter fullscreen mode Exit fullscreen mode

React SSR entry point (resources/js/ssr.jsx):

import ReactDOMServer from 'react-dom/server';
import { createInertiaApp } from '@inertiajs/react';
import createServer from '@inertiajs/react/server';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { route } from '../../vendor/tightenco/ziggy';

createServer((page) =>
    createInertiaApp({
        page,
        render: ReactDOMServer.renderToString,
        title: (title) => `${title} - Your App`,
        resolve: (name) =>
            resolvePageComponent(
                `./Pages/${name}.jsx`,
                import.meta.glob('./Pages/**/*.jsx'),
            ),
        setup: ({ App, props }) => {
            global.route = (name, params, absolute) =>
                route(name, params, absolute, props.initialPage.props.ziggy);
            return ;
        },
    }),
);
Enter fullscreen mode Exit fullscreen mode

Building the SSR Bundle

Add an SSR build script to your package.json:

{
    "scripts": {
        "build": "vite build",
        "build:ssr": "vite build --ssr"
    }
}
Enter fullscreen mode Exit fullscreen mode

The SSR build creates a file at bootstrap/ssr/ssr.js (or wherever your Vite config outputs it). This is a Node.js-compatible bundle that runs on the server.

Running the SSR Server

The SSR server needs to run as a persistent process. On Deploynix, configure it as a daemon:

  • Command: node bootstrap/ssr/ssr.js
  • Directory: Your site's root directory
  • User: deploynix

Deploynix manages this daemon with Supervisor, ensuring it restarts if it crashes.

The SSR server listens on port 13714 by default. Laravel communicates with it internally to render pages on the server before sending the HTML to the client.

Restarting the SSR Server During Deployments

Like Horizon, the SSR server loads your JavaScript code into memory. After deploying new code, you need to restart the SSR process to pick up changes.

Add a restart step to your deployment hook:

# Build steps
npm ci --production=false
npm run build
npm run build:ssr

# Restart SSR (Deploynix's Supervisor will handle the restart)
php artisan inertia:stop-ssr
Enter fullscreen mode Exit fullscreen mode

After the SSR process is stopped, Supervisor will automatically start a new instance with the freshly built bundle.

Asset Versioning

Vite automatically adds content hashes to your asset filenames (e.g., app-BxH7a3C4.js). This ensures browsers always load the latest version of your assets. The Vite manifest file (public/build/manifest.json) maps the original filenames to their hashed versions.

How it works in production:

  1. Vite builds your assets with content hashes.
  2. The manifest file records the mapping.
  3. Laravel's @vite Blade directive reads the manifest and generates the correct and tags.
  4. Browsers cache assets aggressively because the hash changes when the content changes.

Common issues with versioning:

Stale assets after deployment: If users have your old page cached, their HTML references old asset URLs. When those assets are deleted (because only the new hashed versions exist), users see broken pages. Solution: Keep the previous deployment's assets available during the transition. Deploynix's zero-downtime deployments handle this by maintaining the previous release until the new one is fully active.

CDN cache issues: If you use a CDN, purge the cache after deployment or configure your CDN to respect the cache headers set by Nginx. Since asset filenames include content hashes, CDN caching works perfectly as long as you don't strip the hash from the filename.

Troubleshooting Common Issues

"Unable to locate file in Vite manifest"

Symptom: Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest

Cause: The Vite manifest file doesn't exist because assets haven't been built.

Solution: Run npm run build on the server. If you're seeing this after deployment, verify your deployment hook is running the build step successfully. Check the deployment logs in Deploynix for errors.

SSR Process Crashes on Startup

Symptom: The SSR daemon keeps restarting and the Supervisor logs show JavaScript errors.

Common causes:

  • Missing Node.js modules. Ensure npm ci runs before the SSR build.
  • Incompatible Node.js version. Verify the server's Node.js version matches your development environment.
  • Browser-only APIs used in SSR context. Code that references window, document, or localStorage will crash in the Node.js SSR environment.

Solution for browser-only APIs:

Guard browser-specific code:

// In Vue components
import { onMounted } from 'vue';

onMounted(() => {
    // This only runs in the browser, not during SSR
    window.addEventListener('scroll', handleScroll);
});
Enter fullscreen mode Exit fullscreen mode
// In React components
import { useEffect } from 'react';

useEffect(() => {
    // This only runs in the browser, not during SSR
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
}, []);
Enter fullscreen mode Exit fullscreen mode

Hydration Mismatches

Symptom: Console warnings about hydration mismatches. The server-rendered HTML doesn't match what the client renders.

Common causes:

  • Date/time formatting that differs between server and client.
  • Randomized content (random IDs, shuffled lists).
  • Browser-only data (screen size, user agent) used in the initial render.

Solution: Ensure your components produce the same output regardless of whether they're running on the server or client during the initial render. Move browser-dependent rendering into lifecycle hooks that only run on the client.

Slow SSR Rendering

Symptom: Initial page loads are slower with SSR enabled than without.

Possible causes:

  • The SSR process is under-resourced (not enough memory or CPU).
  • Complex components with heavy computation during render.
  • External API calls during SSR.

Solution: Profile your SSR rendering to identify bottlenecks. Increase the daemon's available memory. Avoid external API calls during the server render; use Inertia's deferred props or lazy data loading instead.

Performance Optimization Tips

Code splitting: Inertia automatically code-splits by page component. Each page loads only the JavaScript it needs, reducing initial bundle size.

Preloading: Use Inertia's link component with prefetching to load the next page's data before the user clicks:

Users
Enter fullscreen mode Exit fullscreen mode

Asset optimization: Configure Vite to optimize your production build:

export default defineConfig({
    build: {
        rollupOptions: {
            output: {
                manualChunks: {
                    vendor: ['vue', '@inertiajs/vue3'],
                },
            },
        },
    },
});
Enter fullscreen mode Exit fullscreen mode

This separates vendor libraries into their own chunk, which changes less frequently and can be cached longer by browsers.

Conclusion

Deploying an Inertia.js application on Deploynix requires attention to three areas: the asset build pipeline, SSR process management, and asset versioning. Deploynix's deployment hooks provide the right integration point for your build commands. Daemon management keeps your SSR process running reliably. And zero-downtime deployments ensure your users never see a broken page during the transition.

Start without SSR to keep things simple. Add SSR when SEO or initial load performance becomes important. Configure your deployment hooks once, and every subsequent deployment automatically handles the build process. The result is a modern, interactive Laravel application with the performance characteristics of a server-rendered site.

Top comments (0)