Web Solutions

How to Deploy a Laravel App to cPanel Shared Hosting Safely

Deploy a Laravel app to cPanel shared hosting safely: code outside public_html, document root on public/, .env, permissions, MySQL, cron and common errors.

GPTLabAI team 7 min read

You can deploy a Laravel app to cPanel shared hosting safely if you follow one rule: the application code lives outside public_html, and only Laravel’s public/ folder is exposed to the web. After that, it is a matter of setting up .env, the database, file permissions and a cron job for the scheduler. This guide walks through each step, the choice between SSH and FTP deploys, and the errors we see most often.

Before you start: check the hosting

Not every shared host can run a current Laravel version well. As of September 2026, Laravel 13 is the current major release and the default laravel/laravel skeleton requires PHP 8.3 or newer. Check the following in cPanel before you upload anything:

  • PHP version: select 8.3 or newer in MultiPHP Manager or Select PHP Version.
  • PHP extensions: Laravel lists ctype, curl, dom, fileinfo, filter, hash, mbstring, openssl, pcre, pdo, session, tokenizer and xml, and for MySQL you also need pdo_mysql. See the Laravel deployment docs for the current list.
  • SSH access: many hosts offer it but leave it off by default. It makes everything easier.
  • Composer: often available over SSH; if not, you will build vendor/ locally or in CI.
  • Document root control: whether you can change the document root of the main domain or only of addon domains and subdomains.

The safe folder layout

The most common mistake is uploading the whole Laravel project into public_html. That can expose .env, storage/ and vendor/ to anyone who guesses the path. Use this layout instead:

/home/youruser/
├── laravel-app/          ← the whole project (app, config, vendor, .env, storage…)
│   └── public/           ← the only folder the web server should serve
└── public_html/          ← either the document root is changed, or this becomes a thin shim

Option A: point the document root at public/ (preferred)

In cPanel, go to Domains, edit the domain and set the document root to laravel-app/public. For addon domains and subdomains this is usually straightforward. Some hosts also allow it for the primary domain; others lock it to public_html.

If you have SSH and the host allows symlinks, replace public_html with a link:

cd ~
mv public_html public_html_old
ln -s ~/laravel-app/public ~/public_html

Option C: copy public/ into public_html (last resort)

If neither is possible, copy the contents of public/ into public_html and edit public_html/index.php so its paths point to the app folder:

require __DIR__.'/../laravel-app/vendor/autoload.php';

$app = require_once __DIR__.'/../laravel-app/bootstrap/app.php';

Also check maintenance.php in the same file. This works, but every deploy now has to update two places, so we treat it as a fallback.

Configure .env for production

Create .env on the server from .env.example. Never commit it and never upload your local one unchanged.

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

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=youruser_appdb
DB_USERNAME=youruser_appuser
DB_PASSWORD=use-a-long-random-password

LOG_CHANNEL=daily
SESSION_DRIVER=database
QUEUE_CONNECTION=database

Then generate the key once:

php artisan key:generate

APP_DEBUG=false is not optional. With debug on, an error page can show environment variables, including your database password.

Set up MySQL in cPanel

  1. Open MySQL Databases (or the Database Wizard).
  2. Create a database. cPanel prefixes it with your account name, for example youruser_appdb.
  3. Create a user with a strong password, also prefixed.
  4. Add the user to the database and grant only the privileges the app needs. For most apps that means data and schema privileges, not everything.
  5. Use localhost as DB_HOST unless your host documents otherwise.

Run migrations over SSH:

cd ~/laravel-app
php artisan migrate --force

The --force flag is required in production because Laravel asks for confirmation otherwise. Take a database backup before running migrations on a live site.

Laravel writes to storage/ and bootstrap/cache/. On most cPanel servers PHP runs as your own user, so standard permissions are enough:

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

Do not set 777. If writes fail with 755, the host is running PHP as a different user, and you should ask support rather than opening permissions to everyone.

If the app serves user uploads from the public disk, create the symlink:

php artisan storage:link

Some shared hosts disable PHP’s symlink() function. In that case create the link manually with ln -s ~/laravel-app/storage/app/public ~/laravel-app/public/storage, or ask support.

Optimise for production

After each deploy, cache configuration, routes, views and events:

composer install --no-dev --optimize-autoloader
php artisan optimize

php artisan optimize caches config, routes, views and events in one step. Remember that once config is cached, changes to .env are ignored until you run it again (or php artisan optimize:clear).

Run the scheduler with cron

Laravel’s scheduler needs one cron entry that runs every minute. In cPanel, open Cron Jobs and add:

* * * * * cd /home/youruser/laravel-app && /usr/local/bin/php artisan schedule:run >> /dev/null 2>&1

The PHP path varies between hosts. Run which php over SSH, or check your host’s docs, and make sure the CLI PHP version matches the one your site uses. A frequent surprise is a web version of 8.3 while cron quietly runs an older CLI binary.

Shared hosting rarely allows long-running processes, so a permanent queue:work daemon is usually not an option. A practical workaround is to process the queue from the scheduler:

// routes/console.php
use Illuminate\Support\Facades\Schedule;

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

SSH vs FTP deploy

SSH (git or rsync) FTP / FTPS
Speed Fast, only changed files Slow for vendor/ with thousands of files
Run artisan and composer Yes No, needs a workaround
Atomic-ish releases Possible with release folders and a symlink No, files change one by one
Availability Needs SSH enabled Works on almost every host
Our preference Yes Only when SSH is not available

With SSH, a simple deploy looks like this:

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

With FTP only, build the app in CI (including vendor/ and compiled front-end assets), upload it with an FTP deploy action that syncs only changed files, and exclude .env and storage/ so they survive each deploy. Migrations then need either a one-off SSH session, the cPanel terminal, or a protected route you remove afterwards. We explain the CI side in push-to-deploy with GitHub Actions.

Common errors and fixes

500 error with a blank page. Check storage/logs/laravel.log and the cPanel Errors page. Usually it is permissions, a missing vendor/ folder, or the wrong PHP version.

“No application encryption key has been specified”. Run php artisan key:generate, then php artisan optimize if config is cached.

Changes to .env do nothing. Config is cached. Run php artisan optimize:clear then php artisan optimize.

404 on every route except the home page. The .htaccess file from public/ is missing or mod_rewrite rules are not applied. Make sure public/.htaccess was uploaded (hidden files are easy to skip in FTP clients).

“SQLSTATE[HY000] [1045] Access denied”. Wrong database name or user, usually because the cPanel prefix is missing, or the user was never added to the database.

CSS and JS return 404. Front-end assets were not built. Run npm run build locally or in CI and upload public/build.

Uploaded images do not show. storage:link was not created, or it points to a local path from your laptop.

Scheduler never runs. Wrong PHP binary or path in the cron entry. Temporarily log output to a file instead of /dev/null to see the error.

Key takeaways

  • App folder outside public_html; only public/ is web-accessible
  • PHP version and extensions match Laravel’s requirements
  • Production .env created on the server with APP_DEBUG=false
  • Database and user created with prefixes and limited privileges
  • storage/ and bootstrap/cache/ writable without 777
  • storage:link in place if you serve uploads
  • php artisan optimize after each deploy
  • One cron entry for schedule:run, with the correct PHP binary
  • SSH deploys where possible; FTP deploys exclude .env and storage/
  • Backup before every migration

Shared hosting is a perfectly reasonable home for many Laravel apps, as long as it is set up with the same care as a VPS. If your host’s setup is fighting you, or a deploy has left the site down, our cPanel hosting support team can sort out the layout, cron and deploy pipeline with you. Contact us with your hosting details and we will take a look.

Have a project in mind? Let’s talk.

Whether you run a business or a research group, tell us what you need built, fixed or evaluated. You get a free consultation and a clear written estimate — no obligation.

  • Free consultation
  • Written scope and estimate
  • We reply within one working day
Contact us