DEV Community

Jotcomponents
Jotcomponents

Posted on

Laravel deployment with Dynamic Application Version Switching

Introduction

Most Laravel deployment procedures assume one of two extremes:

  • full root access on a dedicated server (and run composer install directly on the box)
  • cPanel-style shared host where you shuffle a public/ folder into public_html and call it directly.

Neither model fits a workspace that hosts multiple in-flight branches of the same Laravel 11/12 application and needs to switch the live code version without restarting Apache or moving gigabytes of vendor/.

This article describes other deployment topology where the application is split into a private core (above the web root) and a web entry point (the only thing the web server can see), connected by a small PHP orchestrator that rewires three symlinks during deployment of new version of the application. The result is a system where switching the running version of the application is a one-line constant change in a single file.

The Separation Model: Private vs Web

Every deployment (target) workspace follows the same layout:

/var/www/<workspace>/
├── private/        ← Application Core (Outside Web Root)
│   ├── app/
│   ├── bootstrap/
│   ├── config/
│   ├── database/
│   ├── public/      ← Laravel's own public/ (used as symlink target)
│   ├── resources/
│   ├── routes/
│   ├── storage/
│   ├── vendor/
│   ├── build/       ← Vite Asset Manifest & Bundles
│   └── ...
├── private2/        ← A second full copy for app version switching
│   (folder structure same as in `private` folder)
│
├── web/             ← Apache DocumentRoot (public_html)
│   ├── index.php    ← Modified entry point (with app version switch)
│   ├── symlinksSetup.php    ← Symlink Orchestrator
│   ├── .htaccess
│   ├── storage → '../upload/'      (symlink, created by orchestrator)
│   ├── build → '../private/build/' (symlink, created by orchestrator)
│   └── ...
└── upload/          ← Shared storage target (used for media assets)
Enter fullscreen mode Exit fullscreen mode

During a development Vite emits its production bundle to public/build/. For deployment is a production bundle moved to private/build/ (next to the source code it belongs to 'private or private2'). The orchestrator mirrors that link into web/build so Blade views can resolve @vite() references normally.

Here is the simplified shorthand text summarizing the deployment transformation process, tailored for experienced web application workers:

Deployment Transformation Overview

To prepare the application for runtime switching, the deployment script constructs the private and web directories through a structured transformation process.

  • Application Filtering: The script scans the application root and copies only the essential files into the deploy/private directory. It explicitly strips out development tooling and non-essential directories (such as tests, documentation, and node dependencies) using a predefined exclusion list to keep the deployable artifact clean.

  • Asset Relocation: Hashed frontend bundles produced by Vite are moved from the standard public build directory into deploy/private/build/. This ensures the compiled production assets reside securely alongside the application core.

  • Environment Injection: Target-specific overrides are layered into the build without polluting the source repository. Custom configuration files—such as the target's .env, .htaccess, and a modified index.php configured for application switching—are directly merged into the respective deploy/private/ and deploy/web/ directories.

  • Archive Generation: Finally, the script packages the prepared directories into two timestamped archives (private and web) per environment. These archives are then ready to be transferred and extracted on the target staging or production servers.

The Runtime Orchestrator

This is the code that turns a static private//private2/ pair into a switchable runtime. The version that ships as a test fixture in this
repository lives at web/symlinksSetup.php on the host.

Three responsibilities

The function performs three jobs in order:

  1. Validate the request — refuse to do anything if PRIVATE_CHANGE is not set in .env or if the requested switch value isn't recognised.
  2. Map the requested instance (private or private2) to three concrete symlink definitions: web, storage, and build.
  3. Reconcile the filesystem — if any of the three links is missing or points to the wrong target, tear down all six (the three for each instance) and rebuild the three for the active one.

When a switch is actually required, the function unlinks the three candidate links for both instances before creating the three it needs. That ordering matters: because web/build and web/storage are nested paths, any stale link from the inactive instance is removed first so we never create a symlink whose parent has already been claimed by the wrong target.

The Bootstrap Chain in index.php

The orchestrator is only useful if it runs before the Laravel kernel touches the filesystem. The entry point is the customised web/index.php:

define('LARAVEL_START', microtime(true));

require __DIR__.'/symlinksSetup.php';
const APP_SWITCH = 'private'; // Change this to 'private' or 'private2' as needed

symlinksSetup(APP_SWITCH);

// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../'.APP_SWITCH.'/storage/framework/maintenance.php')) {
    require $maintenance;
}

// Register the Composer autoloader...
require __DIR__.'/../'.APP_SWITCH.'/vendor/autoload.php';

// Bootstrap Laravel and handle the request...
(require_once __DIR__.'/../'.APP_SWITCH.'/bootstrap/app.php')
    ->handleRequest(Request::capture());
Enter fullscreen mode Exit fullscreen mode

All three follow-up paths use APP_SWITCH. The maintenance check, the autoloader, and the bootstrap path all interpolate the constant. That uniformity is what makes the single-line switch effective:
changing APP_SWITCH from 'private' to 'private2' rewires every subsequent file access with no other edits.

Summary

The orchestration is small on purpose: one PHP function, one .env flag, one constant. That is the entire surface area that turns a shared-host-friendly Laravel layout into a runtime-switchable blue/green workspace.

A more detailed description of the code used and the method of deploying Laravel web applications is provided in the article Laravel 11+ on a Hosting Server with Dynamic Application Version Switching.

Top comments (0)