Skip to main content

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:

  1. 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).
  2. Runs composer install --no-dev --optimize-autoloader inside the staging copy, so your working vendor/ (Pest & co.) is never touched.
  3. Uploads the tree with rsync --delete (SSH) or lftp 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 under storage/, artisan caches in bootstrap/cache/, and the public/storage symlink.
  4. With --init (first deploy): renders .env from env.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.
  5. Post-deploy: fixes permissions on storage/ and bootstrap/cache/, runs migrate --force, creates the public/storage link, and artisan 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:

ProtocolWhenNotes
rsynchost offers SSHBest: fast incremental sync, post-deploy over SSH. Needs an accepted SSH key.
sftpmost cPanel hostslftp over SFTP. Connect once interactively first to accept the host key.
ftpplain FTP onlyWorks, 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 untouchable public_html. The script keeps the app at ~/<REMOTE_DIR> (outside the docroot) and syncs public/ into public_html/ with a patched index.php. Nothing inside public_html is ever deleted unless you pass --allow-public-delete; a pre-existing public_html/.htaccess gets 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.