Deploying a Laravel app to cPanel shared hosting with SSH and Git
I recently moved this portfolio, built with Laravel and Filament, to a shared hosting plan that includes cPanel and SSH access. It works well, but a few details are easy to get wrong. This is the path that worked, plus the problems I hit along the way.
What you need first
- A hosting plan with SSH or Terminal, Composer, and Git.
- PHP 8.2 or newer for your domain, selected in MultiPHP Manager.
- A MySQL database and user created in MySQL Databases.
- Your code in a Git repository.
1. Let the server read a private repository
Create a read-only deploy key on the server and add the public half to the repository settings.
ssh-keygen -t ed25519 -f ~/.ssh/id_deploy -N "" -C "hosting-deploy"
cat ~/.ssh/id_deploy.pubPaste the output under Settings, Deploy keys in your repository, without write access. Then tell SSH to use that key for GitHub in ~/.ssh/config.
2. Clone outside the public folder
cd ~
git clone git@github.com:your-user/your-repo.git
cd your-repo
COMPOSER_MEMORY_LIMIT=-1 composer install --no-dev --optimize-autoloaderKeeping the application outside public_html means your .env and source code are never served by the web server.
3. Configure the environment
Copy .env.example to .env, run php artisan key:generate, then set production values: APP_ENV=production, APP_DEBUG=false, your APP_URL, and the database credentials. On shared hosting I use QUEUE_CONNECTION=sync and CACHE_STORE=file, because there is no long-running worker.
Wrap passwords in double quotes. A # or a space can silently truncate a value.
php artisan migrate --force --seed
php artisan storage:linkRun the seeder only once, because it can overwrite content you later edit in the admin panel.
4. Point the domain at the public folder
Laravel must be served from its public directory. If your panel lets you choose a document root for an addon domain, use your-repo/public. Create the domain after cloning, otherwise cPanel creates an empty folder that makes git clone fail. If you cannot change the document root, a symlink from public_html to public does the same job.
Then run AutoSSL from SSL/TLS Status to get a free certificate.
Problems I ran into
- The command line and the website use different PHP versions.
php -vin the terminal said 8.3 while the site was still on 7.4. Verify the web version with a temporary file that printsPHP_VERSION, then delete it. - The sitemap returned a 500 error only on the server. The host had
short_open_tagenabled, so the literal text<?xmlinside a Blade view was parsed as PHP. Emit the XML declaration from the controller instead. - A table page crashed as soon as it had data. A Filament closure used an untyped parameter, so it received
null. Type-hint the record (fn (Model $record) => ...), and test every list page with real rows, not just empty ones. - Pasting several
readprompts at once. The terminal fed the next pasted lines in as answers. Put prompts in a small script and run it separately.
Updating later
Commit and push from your machine, then on the server run git pull followed by a small deploy script that installs dependencies, runs migrations, and rebuilds caches. Never run the seeder again once real content lives in the database.