Introduction
Most Laravel deployment procedures assume one of two extremes:
- full root access on a dedicated server (and run
composer installdirectly on the box) - cPanel-style shared host where you shuffle a
public/folder intopublic_htmland 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)
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/privatedirectory. 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 modifiedindex.phpconfigured for application switching—are directly merged into the respectivedeploy/private/anddeploy/web/directories.Archive Generation: Finally, the script packages the prepared directories into two timestamped archives (
privateandweb) 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:
-
Validate the request — refuse to do anything if
PRIVATE_CHANGEis not set in.envor if the requested switch value isn't recognised. -
Map the requested instance (
privateorprivate2) to three concrete symlink definitions:web,storage, andbuild. - 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());
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)