A robust, secure, and production-ready Laravel package that makes deploying Laravel applications on **cPanel / shared hosting** painless and professional.
awaisjameel/laravel-cpanel-hosting is a Laravel package for a robust, secure, and production-ready laravel package that makes deploying laravel applications on **cpanel / shared hosting** painless and professional..
It currently has 0 GitHub stars and 0 downloads on Packagist (latest version v1.2.0).
Install it with composer require awaisjameel/laravel-cpanel-hosting.
Discover more Laravel packages by awaisjameel
or browse all Laravel packages to compare alternatives.
Last updated
A robust, secure, and production-ready Laravel package that makes deploying Laravel applications on cPanel / shared hosting painless and professional.
Shared hosting doesn't give you SSH-driven CI/CD, so this package exposes a small set of authenticated HTTP endpoints that let a webhook (GitHub, GitLab, Bitbucket, or your own script) drive a deployment: pull the latest code (outside the scope of this package), then hit /deploy to sync the environment, run migrations, rebuild caches, relink storage, and flip maintenance mode — all guarded by a token, an IP allowlist, and optional rate limiting.
X-Deploy-Token header or ?token=) or webhook signature (X-Hub-Signature-256 / X-Gitlab-Token) authentication, optional IP allowlist, optional in-memory rate limiting.GET /deploy runs an ordered list of steps (strings, artisan command arrays, or closures) and stops or continues on failure per your config.storage-link or migrate on their own..env sync — copies a server-side env file (e.g. .env.server) over .env, with automatic timestamped backups and required-key validation (APP_KEY by default).symlink() first, and transparently falls back to a recursive directory copy (with correct file/directory permissions) on hosts where symlink() is disabled.index.php passthrough and hardened .htaccess stubs for cPanel's public_html layout, and (interactively) writes your deploy token/prefix straight into .env.laravel.log.DeployStarting, DeployStepCompleted, DeployCompleted for hooking in notifications (Slack, email, etc.).Schema::defaultStringLength() for older MySQL/MariaDB versions still common on shared hosting (utf8mb4 + short index key limits).composer require awaisjameel/laravel-cpanel-hosting
Run the installer:
php artisan cpanel-hosting:install
This will:
config/cpanel-hosting.php.index.php and .htaccess at your project root (backing up any existing files first), so the app root can be pointed at your Laravel project directory directly instead of public/ on cPanel..env.example if they're missing..env.Installer options:
| Option | Effect |
|-----------------|----------------------------------------------------------|
| --force | Overwrite existing root index.php / .htaccess / config instead of skipping them. |
| --only-config | Publish only the config file, skip the root stubs. |
| --only-root | Install only the root stubs, skip publishing config. |
Non-interactively (e.g. in CI or a deploy script), the command skips the .env prompts and just publishes files:
php artisan cpanel-hosting:install --no-interaction
Publish the config manually if you skipped it during install:
php artisan vendor:publish --tag="cpanel-hosting-config"
Every option reads from an environment variable so config/cpanel-hosting.php rarely needs to be touched directly:
# Core
CPANEL_DEPLOY_ENABLED=false
CPANEL_DEPLOY_TOKEN=
CPANEL_DEPLOY_WEBHOOK_SECRET=
CPANEL_DEPLOY_PREFIX=deploy
CPANEL_DEPLOY_ALLOWED_IPS=
CPANEL_DEPLOY_STOP_ON_FAILURE=true
CPANEL_DEPLOY_LOG_CHANNEL=deploy
# Rate limiting (applied per client IP)
CPANEL_DEPLOY_RATE_LIMIT_ENABLED=false
CPANEL_DEPLOY_RATE_LIMIT_MAX_ATTEMPTS=30
CPANEL_DEPLOY_RATE_LIMIT_DECAY_SECONDS=60
# Env sync
CPANEL_SYNC_ENV_SOURCE=.env.server
CPANEL_SYNC_ENV_TARGET=.env
CPANEL_SYNC_ENV_BACKUP=true
# Storage link
CPANEL_STORAGE_LINK_PREFER_SYMLINK=true
CPANEL_STORAGE_LINK_FALLBACK_COPY=true
CPANEL_STORAGE_LINK_SOURCE=app/public
CPANEL_STORAGE_LINK_PUBLIC_PATH=storage
# MySQL legacy compatibility (utf8mb4 index-length limit)
CPANEL_MYSQL_LEGACY_COMPAT=true
CPANEL_MYSQL_LEGACY_LENGTH=191
CPANEL_MYSQL_LEGACY_ALL_CONNECTIONS=false
# Maintenance mode secret bypass (Laravel's php artisan down --secret=)
APP_MAINTENANCE_SECRET=
| Key | Default | Notes |
|---|---|---|
| enabled | false | Deploy routes only register when this is true. Keep it false until you're ready. |
| token | null | Shared secret compared with hash_equals(). Required unless you're using webhook signatures instead. |
| webhook_secret | null | Enables X-Hub-Signature-256 (GitHub, HMAC-SHA256 over the raw body) and X-Gitlab-Token (direct compare) auth. |
| route_prefix | deploy | Prefix all deploy routes live under. |
| allowed_ips | null | Comma-separated string or array of IPs; when set, only listed IPs may reach deploy routes (checked before auth). |
| rate_limit.* | disabled | A lightweight in-memory (per-request-lifetime) limiter — see Security Notes for why this isn't a substitute for a real throttle. |
| sync_env.source / target | .env.server / .env | Paths are resolved relative to the app base path unless absolute. |
| sync_env.backup | true | Writes {target}.backup.{YmdHis} before overwriting. |
| sync_env.required_keys | ['APP_KEY'] | Sync fails if any of these keys are absent from the synced file. Edit the published config to add more (e.g. DB_PASSWORD). |
| storage_link.prefer_symlink | true | Try symlink() first. |
| storage_link.fallback_copy | true | If symlink() is unavailable or fails, recursively copy instead (with 0755/0644 permissions applied). |
| storage_link.source / public_path | app/public / storage | Resolved via storage_path() / public_path() unless absolute. |
| mysql_legacy_compat.enabled | true | Calls Schema::defaultStringLength() on boot when the active connection driver is mysql/mariadb. |
| mysql_legacy_compat.all_connections | false | When true, checks all configured connections instead of just database.default. |
| pipeline.default_steps | see below | The ordered list of steps GET /deploy runs. |
| pipeline.stop_on_failure | true | Stop the pipeline at the first failed step, or run all steps and report an overall failure. |
| maintenance.secret | null | Passed as --secret to php artisan down, letting you bypass the maintenance page via /?secret=.... |
| logging.channel | deploy | Auto-registered as a single driver writing to storage/logs/cpanel-deploy.log if you haven't defined this channel yourself. |
Once CPANEL_DEPLOY_ENABLED=true, routes are registered under CPANEL_DEPLOY_PREFIX (default deploy):
| Method & Path | Purpose |
|---|---|
| GET /deploy | Runs the full pipeline (pipeline.default_steps). |
| GET /deploy/sync-env | Copies sync_env.source over sync_env.target. |
| GET /deploy/clear | optimize:clear. |
| GET /deploy/migrate | migrate --force. |
| GET /deploy/migrate-fresh | migrate:fresh --force — destructive, drops all tables. |
| GET /deploy/cache | config:cache, route:cache, view:cache, event:cache. |
| GET /deploy/queue-restart | queue:restart. |
| GET /deploy/storage-link | Symlink (or copy-fallback) storage/app/public into public/storage. |
| GET /deploy/maintenance-down | down --retry=60, plus --secret if maintenance.secret is set. |
| GET /deploy/maintenance-up | up. |
| GET /deploy/optimize | optimize. |
| GET /deploy/health | Unauthenticated-payload health check (still requires deploy auth) — returns app_env, timestamp, and route prefix. |
Every endpoint returns a consistent JSON shape:
{
"success": true,
"message": "Deployment pipeline completed.",
"data": { "steps": [ { "step": "sync-env", "result": { "...": "..." } } ] },
"errors": []
}
Deploy routes deliberately bypass the session, CSRF, and default throttle middleware (see routes/deploy.php) since requests come from webhooks/CLI, not a browser session — auth is entirely handled by EnsureDeployTokenIsValid.
Checked in this order by the deploy middleware:
CPANEL_DEPLOY_ALLOWED_IPS) — if set, non-matching IPs get a 403 before auth is even checked.429.X-Hub-Signature-256: sha256=... (HMAC-SHA256 of the raw request body, GitHub-style) or X-Gitlab-Token: <secret>, compared with hash_equals().X-Deploy-Token: {token} header (preferred) or ?token={token} query string, compared with hash_equals().If none of these pass, the route returns 403. If CPANEL_DEPLOY_ENABLED is false, every deploy route returns 404 rather than 403, so an unconfigured install doesn't leak the fact that the routes exist.
pipeline.default_steps accepts a mix of:
sync-env, maintenance-down, optimize-clear, migrate, migrate-fresh, cache, queue-restart, storage-link, maintenance-up, optimize.'artisan:cache:clear' runs php artisan cache:clear, or use an array to pass parameters: ['command' => 'queue:work', 'parameters' => ['--once' => true]].bool or a ['success' => bool, 'message' => string, 'data' => array, 'errors' => array] shape.// config/cpanel-hosting.php
'pipeline' => [
'default_steps' => [
'sync-env',
'maintenance-down',
'artisan:cache:clear',
'migrate',
['command' => 'db:seed', 'parameters' => ['--class' => 'ProductionSeeder', '--force' => true]],
'cache',
'storage-link',
fn () => Http::post('https://hooks.slack.com/...', ['text' => 'Deploy finished!'])->successful(),
'maintenance-up',
],
'stop_on_failure' => true,
],
Listen for these to wire up notifications or auditing:
use Awaisjameel\LaravelCpanelHosting\Events\{DeployStarting, DeployStepCompleted, DeployCompleted};
Event::listen(DeployStarting::class, function (DeployStarting $event) {
// $event->steps, $event->ip
});
Event::listen(DeployStepCompleted::class, function (DeployStepCompleted $event) {
// $event->step, $event->result
});
Event::listen(DeployCompleted::class, function (DeployCompleted $event) {
// $event->success, $event->steps
});
use Awaisjameel\LaravelCpanelHosting\Facades\LaravelCpanelHosting;
LaravelCpanelHosting::isEnabled(); // bool
LaravelCpanelHosting::routePrefix(); // string
public_html)cPanel-style shared hosting typically serves everything under public_html/ directly, but Laravel expects the web root to be public/. The installer's root stubs solve this without a symlink:
index.php — a one-line passthrough (require __DIR__.'/public/index.php') so the app root is your project root..htaccess — blocks direct access to sensitive files (.env, composer.json/.lock, phpunit.xml, artisan) and internal framework directories (app, bootstrap, config, database, resources, routes, storage, tests, vendor), rewrites /public/... requests away, and serves everything else from public/ — with baseline security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, a permissive Content-Security-Policy you should tighten per app).Deploy your Laravel project as-is to public_html/ (or a subdirectory pointed at by your domain), run the installer once, and the app is servable without moving files around or fighting cPanel's document root.
CPANEL_DEPLOY_TOKEN secret and rotate it immediately if it's ever exposed (logs, error trackers, a public repo).X-Deploy-Token) over the query-string token — query strings tend to end up in access logs and browser history.CPANEL_DEPLOY_ALLOWED_IPS whenever your CI/webhook provider publishes a stable IP range.APP_DEBUG=true in production — a failed step's stack trace should not be visible to the public internet.migrate-fresh is destructive — it drops every table. Only include it in your pipeline if you're certain you want that behavior on every deploy (most apps shouldn't).CPANEL_DEPLOY_ALLOWED_IPS or a firewall rule at the host level.composer test
Runs the Pest suite under tests/ (feature tests for deploy routes/middleware, unit tests for SyncEnvAction and StorageLinkAction) via Orchestra Testbench. Also available:
composer analyse # Larastan / PHPStan
composer format # Laravel Pint
composer test-coverage # Pest with coverage
See CHANGELOG.md for recent changes.
Issues and pull requests are welcome at github.com/awaisjameel/laravel-cpanel-hosting.
The MIT License (MIT). See LICENSE for more information.