Shared hosting
Deploys the backend only to a classic shared host (cPanel, DirectAdmin, Plesk & co.) over rsync/SSH or FTP/SFTP — for testing and light use. The admin panel and storefront are not deployed here; they keep their existing hosting (any static host / Node host).
The app runs in a dedicated shared-hosting mode: QUEUE_CONNECTION=sync,
CACHE_STORE=file. Events run in-request and there are no workers — the
deliberate trade-off for running an event-driven modular monolith without
a shell: slightly slower responses instead of a queue worker.
What the deploy script does
infrastructure/sharedhosting/deploy.sh runs on your machine:
- Stages a production tree in a temp dir (no
.git,node_modules, tests, dev.env, local caches/logs — runtime dirs ship as an empty skeleton). - Runs
composer install --no-dev --optimize-autoloaderinside the staging copy, so your workingvendor/(Pest & co.) is never touched. - Uploads the tree with
rsync --delete(SSH) orlftp mirror -R --delete(FTP/SFTP) — incremental, so only changed files transfer after the first deploy. Host-only state is protected from deletion: the remote.env, everything understorage/, artisan caches inbootstrap/cache/, and thepublic/storagesymlink. - With
--init(first deploy): renders.envfromenv.app.example(APP_KEY generated, DB values and APP_URL injected, shared-hosting mode pinned) and uploads it. Later deploys never overwrite the remote.env— edit it in the panel's file manager. - Post-deploy: fixes permissions on
storage/andbootstrap/cache/, runsmigrate --force, creates thepublic/storagelink, andartisan optimize. Over SSH these run directly; without SSH they run through a temporary token-protected PHP runner (see below).
Prerequisites
On the host (check in the panel):
- PHP ≥ 8.3 pinned for the (sub)domain, with the usual Laravel
extensions (
pdo_mysql,mbstring,openssl, …) - A MySQL database + user (shared hosts serve MySQL on
localhost) - Ideally a dedicated subdomain (e.g.
api.example.com) so the docroot can be dedicated to the backend
On your machine: php, composer, rsync, plus ssh (rsync protocol)
or lftp (FTP/SFTP), and curl + openssl when using the no-SSH
post-deploy runner.
The staged
vendor/is built by your local PHP; the host must not run an older PHP than the one you build with.
Configuration
cp .env.sharedhosting.example .env.sharedhosting
$EDITOR .env.sharedhosting # host, user, credentials, remote dirs, app URL
Transport choice:
| Protocol | When | Notes |
|---|---|---|
rsync | host offers SSH | Best: fast incremental sync, post-deploy over SSH. Needs an accepted SSH key. |
sftp | most cPanel hosts | lftp over SFTP. Connect once interactively first to accept the host key. |
ftp | plain FTP only | Works, but the password travels in cleartext — avoid if SFTP is offered. |
Docroot mode:
custom(preferred): the panel lets you point the (sub)domain's document root at~/<REMOTE_DIR>/public. The app is served exactly as designed; nothing is patched.fixed: the docroot is an untouchablepublic_html. The script keeps the app at~/<REMOTE_DIR>(outside the docroot) and syncspublic/intopublic_html/with a patchedindex.php. Nothing insidepublic_htmlis ever deleted unless you pass--allow-public-delete; a pre-existingpublic_html/.htaccessgets overwritten, so re-merge custom rules afterwards.
First deploy
# Rehearse: builds + stages for real, prints every upload instead of running it
./infrastructure/sharedhosting/deploy.sh --dry-run
# The real thing (uploads .env, code, then post-deploy)
./infrastructure/sharedhosting/deploy.sh --init
In custom mode, point the (sub)domain's docroot at
~/<REMOTE_DIR>/public in the panel before (or right after) the first
deploy.
Verify:
curl -s -H 'Accept: application/json' https://api.example.com/api/v1/does-not-exist
# A JSON 404 from Laravel proves the app booted (routes, .env, vendor, PHP).
Subsequent deploys
./infrastructure/sharedhosting/deploy.sh
Syncs changed files, then migrates and re-optimizes. The remote .env
and everything under storage/ (logs, cache, uploaded files) are
untouched.
The scheduler (optional)
Some modules register scheduled tasks (e.g. expiring stale orders in the Order module). To run them, add a cron entry in the panel (Cron Jobs → every minute):
* * * * * cd $HOME/<REMOTE_DIR> && php artisan schedule:run >> /dev/null 2>&1
Replace php with the panel's PHP binary path if the shell default
differs (e.g. /opt/cpanel/ea-php84/root/usr/bin/php).
Post-deploy without SSH
On sftp/ftp hosts there is no shell, so the script temporarily
uploads post-deploy.php under a random name with a one-time
64-hex-char token baked in, calls it once over HTTPS (?token=…&cleanup=1),
checks the DEPLOY_RESULT: OK marker, and deletes it from the server
(the runner also unlinks itself). Without the token it answers 404. It
executes a fixed sequence only — permissions, migrate --force, the
public/storage link, optimize — and can never run arbitrary
commands.
Limits (by design)
- Testing / light use: the sync queue means listeners and listeners-of-listeners run inside the HTTP request.
- No workers, no Redis, no horizontal scale.
- The storefront (Next.js SSR) cannot run on shared hosting at all.