Laravel runs on shared hosting. But not under the assumptions you see in the official docs and in most tutorials. Those docs assume a virtual private server: a root user, permission to install any binary, a service manager that keeps long-running processes alive, and a web root you choose yourself. On shared cPanel you have none of these.

This post is a checklist of what we learned setting up this very store on a shared host. Some of it you could have asked before buying, and some of it only showed up after we hit the wall.

1. The PHP version, and why the command line is not the web#

The first thing to check sounds obvious: which PHP versions does the host offer, and can you change them yourself? Almost every cPanel host has the MultiPHP tool or a "Change PHP version" screen.

But the important point is elsewhere: the version you choose for the web is not necessarily the version that runs in the terminal. On our server the web ran on PHP 8.4, but the php command in the shell called the system's 8.1. That meant php artisan migrate ran on a different interpreter and could break on newer language syntax.

See the difference with these two commands:

Bash
php -v
php84 -v

If the outputs differ, run every artisan command with the explicit version from now on. And if php84 does not exist, find its absolute path:

Bash
ls -d /opt/cpanel/ea-php*/root/usr/bin/php

This has a dangerous follow-up, which we wrote about in the php84 trap in cron jobs: something that works in the terminal may not be found at all in cron.

2. The disable_functions list: the most important thing nobody asks#

Every shared host disables a list of PHP functions for security. That list decides which solutions do not exist for you at all — not "are not recommended", but do not exist.

See it with this command:

Bash
php84 -r 'echo ini_get("disable_functions"), PHP_EOL;'

If exec, shell_exec, system and proc_open are on the list, you can never shell out from PHP to an external binary. Any tutorial that says "just call ImageMagick" or "shrink the video with ffmpeg" is useless to you.

If symlink is blocked too, it gets more serious; the details are here.

3. SSH access or the cPanel terminal#

Some shared plans have no SSH, and some have it but you have to enable it. Recent cPanel versions also include a Terminal tool that opens the same shell in the browser, which is enough for our purposes.

Without a shell, deploying Laravel means uploading files by hand over FTP — you run composer install on your own machine and upload the vendor folder with its thousands of files. It is possible but painful, and every update repeats the cycle.

So ask this before you buy. If the seller does not give a clear answer, it probably means no.

4. Cron: minimum interval and the non-interactive shell#

For scheduling, Laravel needs a cron job that runs every minute:

Code
* * * * * cd /home/USER/app && /opt/cpanel/ea-php84/root/usr/bin/php artisan schedule:run >> /dev/null 2>&1

Check two things:

  • The minimum allowed interval. Some hosts do not allow a cron job every minute and set the minimum to five or fifteen minutes. That means Laravel's standard schedule:run will not be precise, and you have to align your schedule with the real interval.
  • The absolute path of the interpreter. Cron does not read ~/.bashrc, so shortcut names that work in the terminal get "command not found" here.

5. The web root and where the project folder goes#

On cPanel, the web root of the main domain is public_html and you usually cannot change it. Laravel, however, wants only the public folder to be web-accessible, with the rest of the project kept outside.

There are three ways, and only one of them is painless:

ApproachResult
Whole project inside public_htmlThe .env file and the storage folder are reachable from the web. Never do this
Project outside, public_html symlinked to publicBetter, but it needs the symlink function, which may be blocked
Project outside, hand-written index.php in public_htmlWorks and needs no blocked function

The third approach means you put the contents of public in public_html and fix the paths in index.php one level up. That is the structure we use: the project in ~/app and the web root ~/public_html holding only the entry point and public assets. The full details of this structure and the symlink trap are in the storage:link post.

6. MySQL limits that are read-only#

On shared hosting you have no access to MySQL global variables. So any fix that starts with "change this value in the MySQL config file" is closed to you. These are the three you run into most:

  • The index length limit. On utf8mb4 tables each character counts as up to four bytes, and an index on long strings goes over the limit. The result is error 1071; the full fix is here.
  • The sql_mode value. If STRICT_TRANS_TABLES is on, data longer than the column raises an error instead of being truncated. That is good news, not bad — here is why.
  • The innodb_ft_min_token_size value. If it is 3 and you cannot change it, FULLTEXT search will ignore short words (one or two characters) for good, which matters a lot for languages with many short words.

Check their status with a simple query:

SQL
SHOW VARIABLES LIKE 'sql_mode';
SHOW VARIABLES LIKE 'innodb_ft_min_token_size';
SHOW VARIABLES LIKE 'max_allowed_packet';

7. Memory, run time and the counters nobody tells you about#

Get four numbers from the hosting panel or from support:

  • memory_limit — critical for image processing. A twelve-megapixel photo takes many times its file size in memory when GD opens it, because it becomes a pixel array rather than the compressed JPEG bytes.
  • max_execution_time — the time limit for each web request.
  • upload_max_filesize and post_max_size — these two must be consistent, otherwise uploads fail silently.
  • Entry Processes — the number of concurrent requests your account may have. When it fills up, visitors get a 508 error, not a 500, and there is no trace of it in the Laravel log.

Things you inherently do not have on shared hosting#

No trick brings these back. It is better to know from the start so you do not build your architecture on something that does not exist:

  • Long-running processes. There is no always-on queue:work. The practical alternative: the database queue driver and a cron job that runs queue:work --stop-when-empty.
  • WebSockets. You cannot open an arbitrary port.
  • Redis and Memcached are usually not available. Put the cache on file or database.
  • Installing arbitrary PHP extensions. You can only choose from the host's ready-made list.
  • External binaries. No ImageMagick, no ffmpeg, no wkhtmltopdf.

So what is left?#

More than you think. A complete store with an admin panel, file uploads, visit stats and a blog runs within these limits, as long as you fit the architecture to the limits and not the other way around:

  • Image processing with plain GD instead of ImageMagick (how)
  • Search with a normalised column instead of FULLTEXT (why)
  • Heavy work in a nightly cron job instead of a live queue
  • File cache instead of Redis

And when something breaks and the error is vague, first read this post about APP_DEBUG; on a server like this, debug mode can hide the real cause instead of revealing it.

8. Migrations and what should not be automatic#

One decision is better made early: should automatic deployment also run database migrations?

Our answer was no, because on shared hosting rolling back a bad migration is expensive. You have no instant backup access, and restoring from the nightly backup means losing today's data.

Our deploy script does only three things:

Bash
git pull --ff-only
php84 artisan optimize:clear
php84 artisan settings:sync

And migrations run manually and deliberately:

Bash
php84 artisan migrate --force

The --force switch is needed because the environment is production and without it Laravel asks for an interactive confirmation, which a script cannot answer.

Another important point: git pull --ff-only is intentional. If someone changed a file on the server by hand, this command fails instead of merging. A clear failure is much better than a surprise merge on a live site.

9. Backups: what the host gives you is not enough#

Almost every host has periodic backups. Ask two questions before relying on them:

  • How often, and how many copies does it keep? A weekly backup means up to six days of data can be lost.
  • Can you restore it yourself, or do you have to open a ticket? In a real crisis the gap between the two is hours.

Keep your own independent backup too. Since you cannot call mysqldump from PHP, put it in cron, which has a full shell:

Bash
0 3 * * * mysqldump -u USER -pPASS DBNAME | gzip > /home/USER/backups/db-$(date +\%F).sql.gz

Three notes:

  • In cron the percent sign has a special meaning and must be escaped with a backslash, otherwise the command runs only partially.
  • The backup folder must not be inside the web root. A downloadable SQL file means your whole database is public.
  • Delete old copies, or the account's disk space fills up.

If you read the values from a .env file, watch out for the Windows line-ending trap; it was exactly this script that led us to that bug.

10. What you learn last: the firewall#

Many hosts have a bot-protection layer. It is invisible to a normal visitor, but it affects these things:

  • Automated requests to the admin panel may get a CAPTCHA challenge.
  • Site monitoring tools may be blocked and report false results.
  • Rapid back-to-back requests from one IP can be temporarily blocked, even if it is you.

If you plan to automate something — publishing content, syncing, monitoring — know this from the start. The solution is usually to do the work on the server side rather than over HTTP.

Before you buy, three test commands#

If the host has a money-back period, buy the cheapest plan and run these three commands before moving the real project:

Bash
php84 -v
php84 -r 'echo ini_get("disable_functions"), PHP_EOL;'
php84 -r 'echo ini_get("memory_limit"), PHP_EOL;'

If the PHP version is acceptable, the list of disabled functions is short and memory is not below 256 MB, the rest is manageable. Other problems can be worked around; these three cannot.