DEV Community

Cover image for Deploy Laravel on cPanel: Step-by-Step Checklist (2026)
Faizan
Faizan

Posted on Originally published at headsntail.com

Deploy Laravel on cPanel: Step-by-Step Checklist (2026)

You upload your Laravel project to cPanel, open the domain, and get a 500 error instead of your app. Or the homepage loads, but every other route returns 404.

To deploy Laravel on cPanel without those surprises: check the PHP version and extensions, keep the application outside public_html, point the domain at Laravel's public folder, install Composer dependencies, configure .env, then sort out permissions, the storage link, HTTPS, cron and queues. Test everything before real traffic arrives.

Shared hosting can run Laravel, but it isn't a VPS. You may have limited Terminal access, no persistent workers and no control over Apache. This checklist catches those limits before you spend an hour uploading files.

1. Check your cPanel hosting plan before uploading

  • [ ] Confirm the PHP version supports your Laravel release.

Open cPanel → Software → MultiPHP Manager, select your domain, choose the version and click Apply.

Laravel 13 (released 17 March 2026) needs PHP 8.3 or newer and supports PHP 8.3 to 8.5. Laravel 12 supports PHP 8.2 to 8.5.

Verify: MultiPHP Manager shows the right version for the domain. Remember the Terminal's php can be a different version (more on that below).

  • [ ] Check the required PHP extensions.

Laravel 13 needs Ctype, cURL, DOM, Fileinfo, Filter, Hash, Mbstring, OpenSSL, PCRE, PDO, Session, Tokenizer and XML. If Terminal is available:

  php -v   # CLI PHP version
  php -m   # loaded extensions
Enter fullscreen mode Exit fullscreen mode

Verify: Every extension on the list appears in php -m. If one is missing, enable it in PHP Selector (if your host has it) or ask the host.

  • [ ] Check whether Terminal, Composer and cron jobs are available.

Look for Terminal under Advanced and Cron Jobs in cPanel. Some hosts disable shell access or kill long-running processes.

Verify: You can run php -v and composer --version, and you can save a cron entry. Don't assume Terminal is included just because cPanel supports it.

  • [ ] Confirm you can change the domain's document root.

This decides how you'll expose Laravel's public folder.

Verify: Open Domains in cPanel and check whether the document root is editable. Many hosts lock it for the primary domain but allow it for addon domains and subdomains.

2. Deploy Laravel on cPanel with a secure folder layout

  • [ ] Keep the Laravel application above public_html.
  /home/cpaneluser/
  ├── laravel-app/
  │   ├── app/
  │   ├── bootstrap/
  │   ├── config/
  │   ├── database/
  │   ├── public/
  │   ├── resources/
  │   ├── routes/
  │   ├── storage/
  │   ├── vendor/
  │   ├── .env
  │   └── artisan
  └── public_html/
Enter fullscreen mode Exit fullscreen mode

The web should only ever see the contents of Laravel's public folder.

Verify: .env, artisan and composer.json cannot be opened in a browser.

  • [ ] Prefer changing the document root (Option A).

If your host allows it, point the domain straight at:

  /home/cpaneluser/laravel-app/public
Enter fullscreen mode Exit fullscreen mode

This is the cleanest setup: Laravel's own public/index.php and .htaccess stay untouched.

Verify: The domain opens the app, and nothing outside public is reachable.

  • [ ] Use a public-only copy when you can't change the document root (Option B).

Keep the full app in laravel-app, then copy the contents of laravel-app/public (including the hidden .htaccess) into public_html.

Then edit public_html/index.php. A standard Laravel 11 to 13 index.php becomes:

  <?php

  use Illuminate\Foundation\Application;
  use Illuminate\Http\Request;

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

  // Maintenance mode file now lives in the app folder
  if (file_exists($maintenance = __DIR__.'/../laravel-app/storage/framework/maintenance.php')) {
      require $maintenance;
  }

  // Composer autoloader from the app folder
  require __DIR__.'/../laravel-app/vendor/autoload.php';

  /** @var Application $app */
  $app = require_once __DIR__.'/../laravel-app/bootstrap/app.php';

  // Tell Laravel that public_html is the public folder
  $app->usePublicPath(__DIR__);

  $app->handleRequest(Request::capture());
Enter fullscreen mode Exit fullscreen mode

Two lines people miss. The maintenance path: leave it pointing at ../storage and php artisan down stops working with no error. The usePublicPath line: without it, Laravel still thinks the public folder is laravel-app/public, so @vite looks for build/manifest.json there and throws "Vite manifest not found".

Compare this with your own public/index.php and keep anything custom your project added.

Verify: Homepage and a nested route both work, and public_html holds only index.php, .htaccess, assets and build/.

  • [ ] Never point the domain at the project root.

Dumping the whole project into public_html and trying to block folders afterwards isn't a safe substitute. A symlink from public_html to laravel-app/public can also work, but only if the host allows symlinks (FollowSymLinks / SymLinksIfOwnerMatch); test it before relying on it.

Verify: No URL on your domain ever serves a file from the project root.

3. Upload the code: Composer, Vite assets, .env and MySQL

  • [ ] Clean local caches before you zip the project (Windows/XAMPP especially).
  php artisan optimize:clear
Enter fullscreen mode Exit fullscreen mode

Cached files in bootstrap/cache/ contain absolute paths from your machine (like C:\xampp\htdocs\...). Upload them and the live site breaks with path errors. Also make sure the ZIP includes the empty storage/framework/cache, sessions and views folders; if they're missing, Laravel fails with "Please provide a valid cache path."

Verify: bootstrap/cache on the server contains only .gitignore (and later, caches you generate there).

  • [ ] Install production dependencies with Terminal if available.
  cd ~/laravel-app
  composer install --no-dev --optimize-autoloader
Enter fullscreen mode Exit fullscreen mode

This installs exactly what composer.lock lists, without dev packages.

Gotcha: Composer runs on the CLI PHP, which may differ from the domain's PHP in MultiPHP Manager. If the CLI is older, Composer refuses with platform errors; if it's newer, it may install packages the website's PHP can't run. Check:

  php -v
  composer check-platform-reqs --no-dev
Enter fullscreen mode Exit fullscreen mode

If the CLI version is wrong, ask the host how to run Composer with the right PHP. Don't "fix" it with --ignore-platform-reqs; that just moves the crash to runtime.

Verify: vendor/autoload.php exists and check-platform-reqs reports no failures.

  • [ ] Build vendor locally if there's no SSH.

Run the same composer install --no-dev --optimize-autoloader locally with the same PHP major/minor version the domain uses, then upload a ZIP through File Manager and extract it into laravel-app.

Verify: vendor/autoload.php exists on the server and the site doesn't throw class or platform errors.

  • [ ] Build and upload frontend assets.

If the app uses Vite, build locally (npm ci needs a package-lock.json):

  npm ci
  npm run build
Enter fullscreen mode Exit fullscreen mode

Upload public/build to the folder your domain serves: laravel-app/public/build for Option A, public_html/build for Option B. Every time you rebuild, upload it again.

Verify: CSS and JS load with no "Vite manifest not found" error and no 404s in the browser's Network tab.

  • [ ] Create the database and user.

Open MySQL Databases (or Manage My Databases). Create the database, create a user, and add the user to the database with All Privileges. cPanel prefixes both names with your account name, so myapp becomes cpaneluser_myapp.

Verify: The full prefixed names match .env exactly.

  • [ ] Create the production .env.

Start from .env.example, never from your local .env:

  APP_NAME="My Laravel App"
  APP_ENV=production
  APP_DEBUG=false
  APP_URL=https://example.com

  DB_CONNECTION=mysql
  DB_HOST=localhost
  DB_PORT=3306
  DB_DATABASE=cpaneluser_myapp
  DB_USERNAME=cpaneluser_dbuser
  DB_PASSWORD="replace-with-real-password"
Enter fullscreen mode Exit fullscreen mode

Most cPanel hosts use localhost; a few give you a separate database hostname.

Verify: .env sits in laravel-app, can't be opened from a browser, and says APP_DEBUG=false.

  • [ ] Generate the application key.

With Terminal:

  cd ~/laravel-app
  php artisan key:generate
Enter fullscreen mode Exit fullscreen mode

Without Terminal, run this locally instead. --show prints a new key without touching your local .env:

  php artisan key:generate --show
Enter fullscreen mode Exit fullscreen mode

Paste the output into APP_KEY= in the production .env.

Never regenerate an existing production key casually. Sessions get logged out and anything encrypted with the old key becomes unreadable.

Verify: APP_KEY starts with base64: and "No application encryption key has been specified" is gone.

  • [ ] Run migrations safely.

Back up the database first. Then:

  php artisan migrate --force
Enter fullscreen mode Exit fullscreen mode

--force is required because Laravel asks for confirmation in production.

Without Terminal you have two options. Importing a schema through phpMyAdmin works for a fresh install, but it doesn't run PHP logic inside migrations. Alternatively, if the host offers Remote MySQL, whitelist your IP, point a local copy of the app at the live database and run php artisan migrate --force from your machine; remove the IP afterwards.

Verify: The expected tables exist and the app can read and write records.

4. Permissions, storage link and production cache

  • [ ] Set safe permissions.
  Directories: 755
  Files:       644
Enter fullscreen mode Exit fullscreen mode

Laravel must be able to write to storage/ and bootstrap/cache/. On most cPanel servers PHP runs as your own user, so 755 is enough. Don't use 777; on suPHP-style setups it actually causes 500 errors.

Verify: storage/logs/laravel.log gets written and sessions persist across page loads.

  • [ ] Create the public storage link.

For Option A (document root is laravel-app/public):

  php artisan storage:link
Enter fullscreen mode Exit fullscreen mode

This links laravel-app/public/storage to laravel-app/storage/app/public.

For Option B, that default link lands in the wrong folder. Create it inside public_html instead (if your host allows symlinks):

  ln -s /home/cpaneluser/laravel-app/storage/app/public /home/cpaneluser/public_html/storage
Enter fullscreen mode Exit fullscreen mode

Verify: Upload a test image to the public disk and open it at /storage/....

  • [ ] Cache the production configuration.

Once .env is correct:

  php artisan optimize
Enter fullscreen mode Exit fullscreen mode

This caches config, events, routes and views. Two gotchas: route caching fails if any route uses a closure that can't be serialized, and after config:cache, env() returns null anywhere outside the config/ files. If code calls env() directly in a controller, it breaks the moment you cache.

After any .env change, run php artisan optimize again (or php artisan config:clear to drop the cache).

Verify: Routes resolve, settings are correct, and nothing reads null where it shouldn't.

5. Cron jobs, queues and HTTPS

  • [ ] Run the scheduler every minute.

In Cron Jobs, choose Once Per Minute (* * * * *) and use:

  cd /home/cpaneluser/laravel-app && /usr/local/bin/php artisan schedule:run >> /dev/null 2>&1
Enter fullscreen mode Exit fullscreen mode

Your host may use a different PHP path (for example /usr/local/bin/ea-php83). Check it before saving. The cron job generator helps with timing fields for other jobs.

Verify: A test task runs on its own within a minute or two. If nothing happens, work through Laravel schedule:run not working on cPanel.

  • [ ] Use short-lived queue workers if persistent workers aren't allowed.

Set the queue driver:

  QUEUE_CONNECTION=database
Enter fullscreen mode Exit fullscreen mode

New Laravel 11+ projects include the jobs table migration; if yours doesn't, run php artisan make:queue-table and migrate. Then add a second every-minute cron:

  cd /home/cpaneluser/laravel-app && /usr/local/bin/php artisan queue:work --stop-when-empty --max-time=50 >> /home/cpaneluser/laravel-app/storage/logs/queue-cron.log 2>&1
Enter fullscreen mode Exit fullscreen mode

--stop-when-empty exits once the queue is empty; --max-time=50 asks the worker to stop after about 50 seconds, after its current job finishes. A slow job can still overlap with the next cron run. For heavier queues, use hosting with supervised workers.

Verify: Dispatch a test job, see it complete, and check php artisan queue:failed is empty.

  • [ ] Activate a valid SSL certificate.

Check SSL/TLS Status (AutoSSL). Make sure both example.com and www.example.com are covered before forcing HTTPS.

Verify: https://example.com shows a valid padlock.

  • [ ] Redirect HTTP and www to one address.

In the public .htaccess, add this block above Laravel's own rewrite block (for non-www HTTPS):

  <IfModule mod_rewrite.c>
      RewriteEngine On

      RewriteCond %{HTTPS} !=on [OR]
      RewriteCond %{HTTP_HOST} ^www\.example\.com$ [NC]
      RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]
  </IfModule>
Enter fullscreen mode Exit fullscreen mode

Replace example.com with your domain. Behind Cloudflare in Flexible SSL mode, Apache always sees plain HTTP, so this rule causes an endless redirect loop; switch Cloudflare to Full (strict) or let Cloudflare do the redirect.

Verify: http://, https://, www and non-www all land on one URL with a single redirect.

  • [ ] Keep Laravel's original rewrite rules.

Don't replace public/.htaccess with a redirect-only file. For Option B, the copied .htaccess in public_html is the one that matters.

Verify: Open /login directly and refresh it; no Apache 404.

  • [ ] Block sensitive files as a second line of defence.
  <FilesMatch "^(\.env.*|composer\.(json|lock))$">
      Require all denied
  </FilesMatch>
Enter fullscreen mode Exit fullscreen mode

This is a safety net, not a substitute for keeping the project outside the web root. For browser-side protections, see security headers in .htaccess on cPanel.

Verify: https://example.com/.env returns 403 or 404, never your settings.

6. Fix common Laravel errors on cPanel and test before launch

Errors you're most likely to hit

  • [ ] 500 Internal Server Error: check permissions, PHP version and the log.

On suPHP-style servers, the error log shows something like:

  Directory is writeable by group
Enter fullscreen mode Exit fullscreen mode

That usually means a folder or file is 775/777. Reset directories to 755 and files to 644. Then check the PHP version, missing extensions, and:

  storage/logs/laravel.log
Enter fullscreen mode Exit fullscreen mode

If laravel.log is empty, the failure happened before Laravel booted; check cPanel's Errors page instead.

Verify: The error log names the original cause and the page loads.

  • [ ] Errors mentioning C:\xampp or another local path.

Cached files from your machine were uploaded. Run on the server:

  php artisan optimize:clear
  php artisan optimize
Enter fullscreen mode Exit fullscreen mode

Verify: No local Windows paths appear in errors or in bootstrap/cache.

  • [ ] 403 Forbidden: check the document root.

The domain points at a folder with no index.php, or Apache can't reach it. Don't fix it by enabling directory listing.

Verify: The domain serves public/index.php (Option A) or public_html/index.php (Option B).

  • [ ] "No application encryption key has been specified."

APP_KEY is missing or the config cache is stale:

  php artisan config:clear
  php artisan config:cache
Enter fullscreen mode Exit fullscreen mode

Verify: The error is gone and existing encrypted data still decrypts.

  • [ ] Uploaded images return 404.

The storage link is missing or in the wrong folder. With Option B, the link must live in public_html.

Verify: A file in storage/app/public opens at its /storage/... URL.

  • [ ] Homepage works but every route returns 404.

Laravel's .htaccess is missing from the served folder (hidden files are easy to skip when copying), or mod_rewrite isn't allowed.

Verify: /login works without index.php in the URL.

  • [ ] Mixed content or wrong http:// links behind Cloudflare.

Set APP_URL to https://.... If HTTPS terminates at a proxy, tell Laravel to trust it. In Laravel 11+, in bootstrap/app.php:

  ->withMiddleware(function (Middleware $middleware): void {
      $middleware->trustProxies(at: '*');
  })
Enter fullscreen mode Exit fullscreen mode

Only trust '*' when traffic really arrives through the proxy; otherwise visitors can fake forwarded headers.

Verify: Developer Tools shows no blocked mixed-content requests.

Test the live install before accepting traffic

  • [ ] Homepage and direct routes. Open the homepage, /login and a page that queries the database. No 403, 404 or 500.
  • [ ] Database writes and sessions. Log in, create a test record, reload, log out. The record persists and CSRF doesn't reject valid forms.
  • [ ] Uploads, scheduler and email. Upload an image, let a scheduled task run, send a test email. If mail arrives in spam, fix authentication with SPF, DKIM and DMARC on cPanel.
  • [ ] Production settings. APP_DEBUG=false, config cached after the final .env, HTTPS working, /.env blocked. The built-in health route /up should return 200.

Three details people miss most

  • [ ] The document root must serve only Laravel's public files. Check the folder layout before anything else.
  • [ ] Terminal PHP may not match web PHP. MultiPHP Manager sets the domain's version; the CLI can be another one. Check both when Composer works but the site doesn't.
  • [ ] Cron and queues need their own setup. A working homepage says nothing about scheduled tasks, emails or jobs. Trigger each one and check its result.

If you'd like a second pair of eyes on a hosting setup or deployment error, contact HeadsNTail with the error message and hosting details.

FAQ

Can I deploy Laravel on cPanel without SSH?

Yes. Build vendor and the Vite assets locally with the same PHP version, upload a ZIP through File Manager, create the database in cPanel, and generate the key locally with php artisan key:generate --show. Migrations and cache commands get harder; use Remote MySQL or a phpMyAdmin import, and never leave a public PHP script that runs Artisan commands.

Which PHP version is required for Laravel 13?

Laravel 13 needs PHP 8.3 or newer and supports PHP 8.3 to 8.5. Laravel 12 supports PHP 8.2 to 8.5; its bug-fix support ended on 13 August 2026 and security fixes continue until 24 February 2027. Set the version per domain in MultiPHP Manager and check the required extensions.

How do I run Laravel migrations without cPanel Terminal?

For a fresh install, export the schema locally and import it through phpMyAdmin, but that skips any PHP logic inside migrations. If your host offers Remote MySQL, allow your IP, run php artisan migrate --force from a local copy pointed at the live database, then remove the IP. Back up first either way.

Why does Laravel work locally but show a 500 error on cPanel?

The usual causes are a different PHP version, a missing extension, 777 permissions, a wrong .env, a missing vendor folder, or cached config uploaded from your own machine. Check cPanel's error log and storage/logs/laravel.log, and I'd check the PHP version and permissions before touching application code.

Before you deploy Laravel on cPanel, confirm the PHP version, the extensions and whether you can change the document root. Those three answers decide which path in this checklist you follow.


Originally published at headsntail.com. Free tools for PHP, Laravel and cPanel work: headsntail.com/tools.

Top comments (0)