Skip to content
Guides & Tutorials

How to Deploy Laravel on cPanel Shared Hosting (Document Root, Cron and Queues)

In this article12 sections

Laravel runs well on cPanel hosting, but it does not behave like a WordPress site you upload and forget. The framework expects its web server to serve one folder, public, while the rest of the application stays out of reach, and cPanel’s default layout puts everything in public_html. Most failed deployments come from that mismatch, from the wrong PHP version on the command line, or from trying to run a queue worker as a permanent process on a plan that does not allow it. This guide walks through a safe layout, the document root change, the .env file, migrations, the scheduler and queues through cron, and the errors people hit most. It assumes Laravel 12, shell access through SSH or cPanel’s Terminal, and a shared or reseller plan.

What you need before you start

Laravel 12 needs PHP 8.2 or newer, plus a short list of PHP extensions: Ctype, cURL, DOM, Fileinfo, Filter, Hash, Mbstring, OpenSSL, PCRE, PDO, Session, Tokenizer and XML. Set the PHP version for the domain in cPanel’s MultiPHP Manager, or in Select PHP Version on CloudLinux servers, and enable the extensions in the same place. You also need a MySQL database and user, which you create in cPanel under Databases.

Know the limits of shared hosting before you design anything:

Feature On shared cPanel hosting
Web requests, migrations, Artisan commands Work normally through SSH or Terminal.
Scheduler through one cron job Works, because cron starts it and it exits quickly.
Queued jobs through cron Work, with a short-lived worker started every minute.
Always-on queue worker, Horizon, WebSockets Usually not allowed. Long-running processes are normally killed, so these need a VPS.
Cron more often than once a minute Not possible. Some hosts also set a longer minimum interval, so check yours.

Where the code should live

Laravel’s own documentation is firm on one point: the web server must send every request to public/index.php, and you should never move index.php to the project root, because that exposes configuration files to the internet. On cPanel that means keeping the application in its own folder and serving only its public directory. A clean layout looks like this:

/home/username/laravel-app/          application code, .env, vendor, storage
/home/username/laravel-app/public/   the only folder the web serves
/home/username/public_html/          left for other sites, not for this app

Replace username with your cPanel user. The goal is that a visitor can never request .env, storage or vendor by URL.

Step 1: Upload the code and install dependencies

Upload the project to /home/username/laravel-app with Git, File Manager or SFTP, without the vendor or node_modules folders. Then, in Terminal or SSH, install the PHP dependencies for production:

cd ~/laravel-app
composer install --no-dev --optimize-autoloader

If Composer is killed or runs out of memory, your account has probably hit a memory or process limit. In that case run the same command on your own computer, upload the finished vendor folder and continue. If your project uses Vite, run npm run build locally and upload the built files under public/build, because you do not need Node.js on the server to serve them.

Step 2: Point the document root at public

For a subdomain or an addon domain, open cPanel, go to Domains, choose Manage next to the domain, and set the document root to laravel-app/public. The target folder must already exist, and cPanel does not move files for you. To undo it, set the old path again. Two points from cPanel’s documentation matter here:

  • This change works for subdomains and addon domains. For the account’s primary domain, the host has to change it from WHM, so you will need to ask them.
  • AutoSSL renewals can fail if the new document root is empty or missing, so do not point the domain at a folder before it has files.

Some servers restrict document roots to public_html, through a setting called Restrict document roots to public_html. In that case, put the project in a folder inside public_html, for example public_html/myapp, and set the document root to public_html/myapp/public. This is workable but riskier: if your primary domain also serves public_html, a visitor can request yourdomain.com/myapp/.env directly. Test that URL. If it returns the file, block it or move the app out of public_html.

Step 3: Configure .env and the database

Create the database and a database user in cPanel, add the user to the database with all privileges, and note that cPanel prefixes both names with your account name. Then create .env in the application folder, not in public:

cd ~/laravel-app
cp .env.example .env
php artisan key:generate

Edit .env and set at least these values:

APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com
DB_CONNECTION=mysql
DB_HOST=localhost
DB_DATABASE=cpaneluser_laravel
DB_USERNAME=cpaneluser_laraveluser
DB_PASSWORD=use-a-long-random-password
QUEUE_CONNECTION=database

Leave APP_DEBUG set to false in production. With it on, an error page can print configuration details to visitors.

One warning about the command line: on many cPanel servers the plain php command is not the version your site uses. Check before you run Artisan:

php -v
ls /usr/local/bin | grep ea-php

If php -v shows a version below 8.2, call the right binary by its full path, for example /usr/local/bin/ea-php83 artisan migrate. On CloudLinux servers using alternative PHP, the binary is often under /opt/alt/, for example /opt/alt/php83/usr/bin/php. Use the same full path in cron jobs.

Run the database migrations, create the public storage link, and let Laravel cache its configuration, events, routes and views:

php artisan migrate --force
php artisan storage:link
php artisan optimize

The optimize command builds all of those caches at once. If you cache configuration, Laravel’s documentation warns that the env() function must only be called inside configuration files, because after caching the .env file is no longer read. If you change .env later, run php artisan config:clear or php artisan optimize again, or your edit will seem to do nothing. Some shared hosts block symbolic links, which makes storage:link fail. If it does, ask the host to allow the link.

Step 5: Add cron for the scheduler and queues

Laravel needs one cron entry, which runs its scheduler every minute. In cPanel open Advanced, Cron Jobs, choose Once Per Minute and use:

cd /home/username/laravel-app && /usr/local/bin/ea-php83 artisan schedule:run >> /dev/null 2>&1

Replace the PHP path with the one you found in Step 4. Without a permanent worker, you can still process queued jobs by letting the scheduler start a short-lived one each minute. In Laravel 12 you define it in routes/console.php:

use IlluminateSupportFacadesSchedule;

Schedule::command('queue:work --stop-when-empty --max-time=50')
    ->everyMinute()
    ->withoutOverlapping();

The worker handles whatever is waiting and exits, either when the queue is empty or after 50 seconds, so the next minute’s run starts clean. This suits email, notifications and imports. Jobs that must run instantly, or that run for many minutes, need a VPS.

Permissions and security checks

The storage and bootstrap/cache folders must be writable by your account. On cPanel your PHP runs as your own user, so the usual modes are enough:

find ~/laravel-app/storage ~/laravel-app/bootstrap/cache -type d -exec chmod 755 {} ;
find ~/laravel-app/storage ~/laravel-app/bootstrap/cache -type f -exec chmod 644 {} ;

Never use 777. Then check from outside that nothing private is public: request https://example.com/.env, https://example.com/storage/logs/laravel.log and https://example.com/vendor/autoload.php. All three should return an error, not file contents. Also keep your .env out of Git, and rotate any password you ever committed.

Deploying updates

A safe update takes a minute and avoids serving a half-updated site:

cd ~/laravel-app
php artisan down
git pull
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimize:clear
php artisan optimize
php artisan up

If the migration fails, fix it before running php artisan up, and keep a database backup from before the update.

Troubleshooting the usual failures

Symptom Likely cause What to check
Blank page or generic 500 error PHP error hidden by APP_DEBUG=false Read storage/logs/laravel.log, and the error log in cPanel Metrics.
Composer says it requires a newer PHP Command line uses an older PHP than the site Use the full PHP path from Step 4.
vendor/autoload.php not found Dependencies not installed Run composer install, or upload vendor built locally.
Home page works, other routes return 404 Document root is not the public folder, or public/.htaccess is missing Check the document root in Domains and that .htaccess exists in public.
No application encryption key APP_KEY is empty, or config cache is stale Run key:generate, then config:clear.
.env changes ignored Configuration cached Run php artisan config:clear.
Uploaded images return 404 Storage link missing Run php artisan storage:link, or ask the host to allow symlinks.
Queued jobs never run Cron path or PHP version wrong, or no worker scheduled Test the cron command in Terminal, and confirm QUEUE_CONNECTION.
Composer or Artisan killed Account memory or process limit Check CPU and Concurrent Connection Usage in cPanel, or build vendor locally.

When shared hosting is not enough

Move to a VPS or dedicated server when you need an always-on queue worker, Horizon, WebSockets, sub-minute scheduling or Redis, or when your app regularly hits account limits. For a first deployment, a website and a small API, a well-configured cPanel hosting plan is a sensible place to start.

Related guides: choosing the PHP version and handler in cPanel, running Node.js and Python apps with the CloudLinux Selector and hosting several PHP apps on one cPanel account safely. If you move to your own server, see hosting multiple websites on one VPS with Nginx and backing up MariaDB and PostgreSQL on a VPS.

Frequently asked questions

Can I put Laravel directly in public_html?

You can, but you should not. Putting the whole project in the document root makes .env, storage and vendor reachable by URL unless you block them. Keeping the app outside public_html and serving only public is safer and needs no extra rules.

Do I need SSH to deploy Laravel on cPanel?

It is far easier with it. SSH or the cPanel Terminal lets you run Composer and Artisan. Without shell access you must build everything locally and import the database by hand, and you cannot run migrations or cache commands, so ask the host for Terminal access first.

Why does my site work but scheduled tasks never run?

The cron job usually uses the wrong PHP binary or the wrong path. Copy the command from the cron entry, run it in Terminal and read the output. If it works there, check that the cron job exists and is set to once per minute.

Can I run php artisan queue:work as a permanent process?

Not on typical shared hosting, because long-running processes are limited or killed. Use the scheduled, short-lived worker from Step 5, or move to a VPS with a process manager.

The short version: keep the app outside public_html, serve only public, use the right PHP binary, run one cron job and cache after every change.

Share this guide