Laravel on cPanel and DirectAdmin — What Works, What Doesn't, When to Move
Verdict at the top: A vanilla Laravel application — controllers, Eloquent, Blade, Livewire, file-based cache and sessions, cron-driven scheduler, SMTP mail — runs perfectly well on Domain India shared cPanel or DirectAdmin hosting. We host hundreds of them. Where it breaks is the Laravel features that need long-running processes: Laravel Octane, queue workers, Horizon, Reverb websockets, and Redis. Those need a VPS. This article shows you exactly how to ship the parts that work, and tells you straight when you've crossed into VPS territory.
What works, what doesn't — the honest table
| Feature | Shared (cPanel / DA) | VPS |
|---|---|---|
| Vanilla Laravel app (web routes, controllers, Eloquent) | Yes | Yes |
| Blade templates, Livewire, Inertia | Yes | Yes |
| MySQL via cPanel/DA | Yes | Yes (you install it) |
| File-based cache + session driver | Yes | Yes |
| SMTP mail via cPanel mail | Yes | Yes |
Cron-based scheduler ( *) | Yes (1-minute granularity) | Yes (any granularity) |
| LiteSpeed cache for static pages | Yes (LSCache plugin) | Yes (manual setup) |
| Composer install on server | cPanel Pro and above (SSH) | Yes |
| Queue: sync driver | Yes | Yes |
Queue: php artisan queue:work | No (LVE kills long-running PHP) | Yes |
| Laravel Horizon | No (needs queue workers + Redis) | Yes |
| Redis cache / session / queue | No (no Redis daemon access) | Yes |
| Laravel Octane (Swoole / RoadRunner / FrankenPHP) | No (long-running daemon, blocked) | Yes |
| Laravel Reverb (websockets) | No (long-running daemon) | Yes |
| Sub-minute scheduled tasks | No (cron is 1-minute granularity) | Yes (with systemd timers) |
| File uploads > 100 MB | Limited (upload_max_filesize) | Yes (you configure) |
| Custom system libraries (FFmpeg, ImageMagick variants) | EasyApache build only | Yes |
If everything you need is in the "shared" column, you can stop reading here, deploy with Path A or B below, and ship. If two or more rows in the "shared = No" column matter to your app, get a VPS. The intermediate "I'll work around it on shared" path almost never ends well — we'll explain why.
Why long-running processes don't work on shared hosting
This is the part most Laravel tutorials skip. Shared cPanel and DirectAdmin accounts run inside a CloudLinux LVE (Lightweight Virtual Environment) — a kernel-enforced sandbox that limits CPU, memory, process count, and process lifetime per account. The defaults on our shared servers:
nproc(process limit): 100 simultaneous processes per account.pmem(memory limit): 1–2 GB depending on plan.- Idle process killer: long-running PHP / Node processes outside the web server are killed after a few minutes.
A worker like php artisan queue:work is, by design, an infinite loop. It waits for jobs, processes them, then waits again — forever. CloudLinux sees this as an "abandoned" process and reaps it. You can demonstrate this on any shared account: SSH in, run php artisan queue:work --daemon, and watch it die within ~3-5 minutes. The same applies to Reverb's websocket daemon, Octane's Swoole/RoadRunner servers, and any custom long-lived script.
cPanel cron jobs do run, but they exit quickly — that's their contract with CloudLinux. The Laravel scheduler line everyone uses is:
* * * * * cd /home/user/app && php artisan schedule:run >> /dev/null 2>&1That works because schedule:run exits in ~50 ms after dispatching due tasks. It's the workers (schedule:work, queue:work, Horizon supervisors) that don't survive.
What about queue:work --once from cron?
People ask this every month. Yes, you can put * * * * * php artisan queue:work --once in cron as a workaround. It processes one job per minute. For a side project with 5 jobs per day, fine. For anything that processes faster than once a minute, or that needs concurrency, or that has any real-time requirement (password resets, payment retries, notification delivery) — this is a trap.
Common patterns where this falls over:
- Failed-payment retries. Customer's card declines, Razorpay webhook fires, you queue a retry job. With cron-driven
queue:work --once, your customer's retry is delayed up to 60 seconds. Half of them won't wait. - Email confirmations. Account signup → queued welcome mail → 30-60 second delay before delivery. Good enough for marketing, not for OTP/password-reset.
- Order processing. Inventory check, payment capture, fulfillment trigger — all queued. Adds 60s minimum to perceived order latency.
- Webhook fan-out. Stripe/Razorpay/Shopify webhook arrives, you queue 4 follow-up jobs. Cron only processes 1 per minute, so the 4-job pipeline takes 4 minutes.
If any of these describe your app, you've already crossed into VPS territory. Don't fight the cron-once pattern.
What runs fine: the deployment guide
The original deployment guide here was good — keeping it (lightly tightened). Two paths.
Path A — Build locally, upload artifact (works on every shared plan)
- Build the production artifact on your dev machine:
```bash
composer install --no-dev --optimize-autoloader
npm run build # Vite build
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
```
- Zip the project, excluding:
- .git/
- node_modules/
- .env (never upload your dev .env)
- tests/
- Contents of storage/logs/ and bootstrap/cache/ (keep folders, clear contents)
- Upload via cPanel File Manager:
- Upload the zip to /home/YOURUSER/laravel-app/ (deliberately outside public_html).
- Use "Extract".
- Move only the contents of laravel-app/public/ into public_html/.
- Edit public_html/index.php — change the two require __DIR__.'/../...' lines to point to ../laravel-app/bootstrap/app.php and ../laravel-app/vendor/autoload.php.
The point: your app code, .env, vendor/, models, and routes all live outside the public document root. Only public/ is web-reachable.
- Set permissions:
```
storage/ 755 (or 775 if your server needs group-write)
bootstrap/cache/ 755
```
Never chmod 777 on shared hosting. It's a real security risk, not a shortcut.
- Create production `.env` in
/home/YOURUSER/laravel-app/(not inpublic_html/):
```
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
DB_CONNECTION=mysql
DB_HOST=localhost
DB_DATABASE=yourcpaneluser_dbname
DB_USERNAME=yourcpaneluser_dbuser
DB_PASSWORD=yourdbpassword
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync
MAIL_MAILER=smtp
```
Note the cache/session/queue drivers — file-based for shared. QUEUE_CONNECTION=sync means jobs run synchronously inside the request that dispatched them. Fine for low-traffic apps.
- Generate app key, migrate, link storage:
```bash
php artisan key:generate
php artisan migrate --force
php artisan storage:link
```
--force is required in APP_ENV=production to suppress the confirmation prompt. If your plan has cPanel Terminal, run these from there. Otherwise SSH (Pro+) or run them from a temporary script.
Path B — SSH + Composer on server (cPanel Pro and above, all DirectAdmin plans with SSH)
If your plan includes SSH, this is far cleaner:
ssh [email protected]
cd ~
git clone https://github.com/you/yourapp.git laravel-app
cd laravel-app
composer install --no-dev --optimize-autoloader
cp .env.example .env
nano .env # fill in production values (see Path A step 5)
php artisan key:generate
php artisan migrate --force
php artisan storage:link
php artisan optimizeThen in cPanel → Domains, set the document root for your domain to /home/yourcpaneluser/laravel-app/public/. Or symlink:
rm -rf ~/public_html
ln -s ~/laravel-app/public ~/public_htmlFor redeploys, the loop becomes:
cd ~/laravel-app
git pull
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimizeThe scheduler — works fine, with one caveat
The Laravel scheduler is the one long-running-ish thing that does work on shared, because it's invoked from cron and exits quickly. In cPanel → Cron Jobs:
* * * * * cd /home/yourcpaneluser/laravel-app && php artisan schedule:run >> /dev/null 2>&1The caveat: 1-minute granularity. ->everyTenSeconds(), ->everyThirtySeconds() look like they should work, but they require the new schedule:work daemon — which is a long-running process and gets killed by LVE within minutes. On shared, your effective minimum interval is 1 minute. Plan accordingly.
Mail — use SMTP, not mail()
Laravel's default MAIL_MAILER=mail uses PHP's mail() function, which has terrible deliverability on shared hosting (often goes straight to spam). Configure SMTP via your cPanel mailbox:
MAIL_MAILER=smtp
MAIL_HOST=mail.yourdomain.com
MAIL_PORT=465
[email protected]
MAIL_PASSWORD=yourmailpassword
MAIL_ENCRYPTION=ssl
[email protected]
MAIL_FROM_NAME="Your App Name"For volume above ~500 emails/day, switch to SendGrid, Mailgun, AWS SES, or Postmark — providers built for transactional email deliverability. Our Email deliverability deep-dive covers SPF/DKIM/DMARC setup.
Performance on shared — what to actually expect
People assume Laravel on shared is slow. On LiteSpeed-powered cPanel (which is what we run on most servers) with php artisan optimize cached, a typical Laravel + MySQL page renders in 80-180 ms server-side at our region. That's fast enough for sites doing tens of thousands of pageviews per day.
What kills the perception:
- Forgetting `php artisan optimize` after deploy. Without route/config/view caches, you pay a 100 ms penalty per request just bootstrapping the framework.
- N+1 query bloat. Eloquent's lazy loading makes this trivial to introduce. Install Laravel Debugbar in dev, watch query counts. Eager-load relationships with
with(). - `APP_DEBUG=true` in production. Enables stack-trace generation on every error and disables several caches. Set to
falsealways. - Vite build leaving dev assets.
npm run buildproduces fingerprinted, minified assets.npm run devproduces dev HMR assets. Make sure the deploy uses the build output.
If after all four, you're still seeing 800 ms+ response times — your app is doing too much per request, or you've outgrown shared.
When to move to a VPS
Concrete signals. If two or more are true, upgrade now:
- You need real queue workers (Horizon, Sidekiq-pattern dispatch, anything more than
--oncecron polling). - You need Redis for cache, session, or queue (any of the three).
- You need Laravel Octane for performance (Swoole/RoadRunner/FrankenPHP).
- You need Laravel Reverb for websockets / live-update features.
- You see "resource limit reached" or LVE-fault notices in cPanel.
- Your
nprocheadroom is constantly low (the cPanel resource graphs show you near the 100-process ceiling). - You need scheduled jobs more frequent than every 60 seconds.
- Your app is consistently above ~5,000 pageviews per day with response times trending up.
- You need to install custom system libraries (FFmpeg variants, ImageMagick with non-default delegates, ML/Python interop).
- Your team uses Forge / Vapor / Envoyer-style deploy and wants to keep that workflow.
The cheapest VPS path for Laravel: Domain India VPS Starter at ₹553/month (1 vCPU, 2 GB DDR4 RAM, 64 GB NVMe, 2 TB bandwidth). Enough headroom for vanilla Laravel + Redis + 1-2 queue workers + Horizon. Step up to VPS Basic (₹1,105/month) when your queue volume or memory pressure exceeds Starter.
A skeleton VPS Laravel setup post-upgrade:
- Nginx + PHP 8.3 FPM (or Caddy if you prefer auto-SSL).
- MySQL 8 or PostgreSQL.
- Redis for cache + session + queue.
- Supervisord watching
php artisan queue:work(or Horizon). - Cron entry running
schedule:runonce a minute (still the right pattern even on VPS). - Let's Encrypt via certbot or Caddy auto-SSL.
- Sentry / Bugsnag for error tracking (these don't work meaningfully on shared either — same long-running-process limitation).
If you want managed-feeling Laravel hosting without becoming a sysadmin, Domain India PaaS (in beta) gives you Forge-like deploy + auto-SSL on top of containers, with Redis and Postgres add-ons.
Common errors and what they actually mean
`ErrorException: file_put_contents(...): failed to open stream: Permission denied` — most often storage/logs/laravel.log or bootstrap/cache/. Fix: chmod 755 (or 775) on storage/ and bootstrap/cache/ recursively, and ensure ownership is your cPanel user.
`No application encryption key has been specified` — you forgot php artisan key:generate, or your .env is missing entirely. Run the command, refresh.
`SQLSTATE[42000]: Specified key was too long; max key length is 1000 bytes` — old MySQL with utf8mb4 and a Laravel migration trying to index a long string column. Fix: in app/Providers/AppServiceProvider.php's boot():
use Illuminate\Support\Facades\Schema;
Schema::defaultStringLength(191);Re-run migrations.
`The stream or file "/home/.../storage/logs/laravel.log" could not be opened in append mode: Failed to open stream: Permission denied` — same as the first error, more verbose. Same fix.
`HTTP 500` with no useful page — APP_DEBUG=false is hiding the error. Tail storage/logs/laravel.log. If empty, check cPanel → Errors. If still nothing, set APP_DEBUG=true temporarily, refresh once, read the trace, set back to false.
`Class "Redis" not found` — your .env has CACHE_DRIVER=redis or SESSION_DRIVER=redis, but the PHP redis extension isn't loaded (and Redis the server isn't accessible). On shared, switch to file. On VPS, enable the extension via your package manager.
`pcntl_fork() has been disabled for security reasons` — Horizon or queue workers tried to fork. Shared hosting disables pcntl_*. You need a VPS for this workload.
`Maximum execution time of 30 seconds exceeded` — a request took longer than max_execution_time. Either optimize the request, raise the limit via .user.ini (cPanel allows you to set up to 300s), or move heavy work to queues. On shared, queues mean cron-based queue:work --once, which has the limitations described above.
`Class 'finfo' not found` — fileinfo PHP extension not enabled. cPanel → Select PHP Version → Extensions → check fileinfo.
`Allowed memory size of 134217728 bytes exhausted` — PHP memory_limit too low. Add to .user.ini in your project root: memory_limit = 512M. Most shared plans allow up to 512 MB, some up to 1 GB.
`bootstrap/cache directory must be present and writable` — same permission issue as the first errors.
Frequently asked questions
Correct. Swoole and RoadRunner both run as long-lived daemons binding ports — neither permitted on shared. FrankenPHP is the same story. Octane is a VPS-and-above feature.
No, regardless of volume. Horizon is a supervisor process that manages queue workers; the supervisor itself is the long-running thing that gets killed. If you only have a few jobs and 60-second latency is acceptable, use cron-based queue:work --once.
Vapor deploys to AWS Lambda; it's a serverless setup, not hosting on someone else's server. You can absolutely use Vapor while keeping your domain registration with us; we just won't be your hosting provider for that app.
Forge buys you provisioning + deploy automation, ~$19/month on top of your VPS cost. If your team is more than one person, Forge usually pays for itself in saved sysadmin time. If you're solo and like learning Linux, DIY VPS is cheaper. We have customers on both paths.
The shared-hosting story is identical. Laravel 11 dropped some default service providers; Laravel 12 changed pagination internals. Neither affects what runs on shared vs VPS. The threshold is workload, not Laravel version.
Yes. Inertia is just a thin protocol over standard Laravel responses; the heavy lifting is client-side. SSR for Inertia (php artisan inertia:start-ssr) does need a long-running Node process — that's the part that needs a VPS.
Yes, perfectly. Livewire is request/response over AJAX, no long-lived server process. Even Livewire's polling features work fine.
Pulse needs Redis for performant ingestion. On shared with the file driver, it technically runs but is unusable in practice. VPS-and-above for Pulse.
Don't try. cPanel's Setup Node.js App is genuinely useful for actual Node apps, but stuffing a websocket bridge in there to talk to a Laravel backend is the kind of architecture you'll regret in 6 months. If you need websockets, get a VPS.
No. All three are pure web-request scaffolding. Two-factor auth via TOTP works fine. Two-factor via SMS depends on your SMS provider's API, which is a normal HTTP call.
Bottom line
Vanilla Laravel on shared cPanel/DirectAdmin is a perfectly legitimate production setup for sites that don't need real-time, queue-heavy, or cache-heavy patterns. Most marketing sites, brochureware, small SaaS dashboards, internal tools, and content-driven apps fit comfortably in this envelope. We host hundreds of them and they work.
The trap is forcing the modern Laravel feature set — Octane, Horizon, Reverb, Redis-backed queues — onto shared and burning a week of your life on workarounds. The right answer is to recognise the threshold early and move to a ₹553/month VPS, where everything you wanted to do just works.
If you're not sure which side of the line your app sits on, send your composer.json and a description of your features to [email protected] — we'll tell you straight whether shared is enough.
Need a VPS for queue workers, Octane, or Reverb? Domain India VPS Starter ₹553/month — root access, Redis-ready, sized for production Laravel. Get a VPS plan