cPanel deployment walkthrough
Step-by-step production install on shared cPanel hosting — subdomain, document root, upload, installer, database, permissions, cron and first login.
This walkthrough installs Zodiac Tech CRM on shared cPanel hosting (the production target: https://crm.zodiactechsoft.in). It uses the browser installer — the same flow buyers use — and documents the manual artisan equivalents for hosts with Terminal access.
Who this is for
- Super-admin / hosting admin — doing the first install.
- Developer — reproducing a customer's install problem.
Prerequisites:
- cPanel login that can create subdomains, databases, cron jobs and set PHP versions.
- The release zip of the application (or the project folder from your source).
- ~30–60 minutes.
Two supported layouts Preferred: point the (sub)domain's document root directly at the app's public/ folder. URLs then have no subfolder — https://crm.example.com/admin. Fallback: upload into a folder/subdirectory. The shipped root index.php + .htaccess bridge forward into public/, which is exactly how the local XAMPP install works (http://localhost/Lead%20Hub/). URLs then include the subfolder, including API/webhook URLs.
Step 1 — Create the subdomain
-
cPanel → Domains → Create A New Domain (older panels: Subdomains).
-
Enter
crmforcrm.zodiactechsoft.in(adjust to your domain). -
Document Root — type the folder where the app will live, ending in
/public, for example:/home/YOURUSER/crm-app/publicIf cPanel forces
public_html/crmas the root and you cannot change it, that is fine — you are now in the fallback layout; keep reading, Steps 6–7 cover it. -
Create.
Expected result: the subdomain resolves (it may 403/404 until files exist).
Step 2 — Set the PHP version
- cPanel → MultiPHP Manager → select the subdomain → PHP 8.4.
- cPanel → Select PHP Version → Extensions (or MultiPHP INI Editor) and confirm the required extensions are enabled:
pdo_mysql,mbstring,zip,gd,curl,bcmath,fileinfo,intlif offered.
Expected result: PHP 8.4 is active for the domain. (php -v in cPanel Terminal should also say 8.4 if you have terminal.)
Step 3 — Upload the application
Everything lives in one app folder. Upload it to the folder above your document root (e.g. /home/YOURUSER/), so the layout becomes /home/YOURUSER/crm-app/….
3a — What must be in the package
| Item | What it is | Notes |
|---|---|---|
app/, bootstrap/, config/, database/, lang/, resources/, routes/ |
Application code, migrations, translations, views | Required |
public/ |
Web root: entry point, built assets (build/, css/, js/, docs-assets/), and the installer |
Required |
Modules/ |
Runtime module folder (marketplace/module-manager installs land here) | Optional — safe to omit; the app boots fine without it and creates it on first module install |
vendor/ |
Composer dependencies | Required on shared hosting (no Terminal/Composer there). With Terminal you may instead run composer install --no-dev --optimize-autoloader and skip uploading it |
storage/ |
Skeleton runtime folders (logs/cache/framework) | Required; made writable in Step 4; may be empty except .gitkeep files |
artisan, composer.json, composer.lock |
CLI entry + dependency manifest | Required |
index.php, .htaccess, install.php, cron.php |
Root bridge files (.htaccess is a hidden dot-file!) |
Required for the fallback subfolder layout and for the no-Terminal cron runner |
.env.example |
Environment template for reference | Optional — the installer writes .env itself |
phpunit.xml, tests/ |
Automated test suite | Optional — exclude to slim the package on production uploads |
Show hidden files before checking the upload cPanel File Manager hides dot-files by default. Enable Settings → Show Hidden Files and confirm .htaccess is present in the app root. Without it, the fallback (subfolder) layout cannot route — requests 404 no matter what else is correct.
3b — What to leave out
| Exclude | Why |
|---|---|
.env |
Created by the installer on the server. Never ship one — it carries database credentials and keys. |
CREDENTIALS.txt |
Never upload or share this file — it lists login credentials. |
.git/, .claude/, .agents/, .gstack/ |
Version-control and local tooling state. |
node_modules/ |
Not needed at runtime — the built assets already ship inside public/. |
archive/, docs/, docs-internal/, scripts/, tools/, android/, demo-apk/ |
Internal/dev-only material — all QA/audit reports live under archive/ (2026-10-06 cleanup). |
storage/logs/*, storage/framework/{cache,sessions,views}/*, bootstrap/cache/*.php |
Stale runtime output of the machine that built the package — clear it so the new host starts clean (keep the empty bootstrap/cache/ folder + its .gitkeep). |
bash.exe.stackdump, .phpunit.result.cache |
Junk files. |
3c — Upload mechanics
- cPanel → File Manager → navigate to the folder above your document root.
- Upload the package zip, then select it → Extract.
- Confirm the structure:
crm-app/app/,crm-app/public/,crm-app/vendor/,crm-app/index.php, andcrm-app/.htaccess(hidden files on).
Upload the zip, extract on the server Do not extract on your PC and then upload tens of thousands of vendor/ files one by one — it is many times slower. Zip → upload → Extract in File Manager is the fast path on shared hosting. If you prefer FTP, upload the zip and extract over SSH, or compress again before transfer.
If you have Terminal and prefer git/uploads without a zip:
cd ~/crm-app # or wherever the app will live
# upload via your preferred method, then:
php -v # confirm 8.4
Step 4 — Prepare dependencies and write permissions
With Terminal (Composer 2 available):
cd ~/crm-app
composer install --no-dev --optimize-autoloader
The post-autoload-dump scripts automatically run artisan package:discover and artisan filament:upgrade, so Filament's assets and caches are wired correctly.
Without Terminal (typical shared hosting): skip composer install — upload the vendor/ folder from your build machine as part of the zip, or via FTP/File Manager. A vendor/ built by the same PHP version line is required (PHP 8.4).
Then make the app writable. In File Manager, set:
storage/— recursively, permission 775bootstrap/cache/— 775public/— 775 (writes per-tenant branding CSS intopublic/tenant/<id>/on settings save)
# Terminal equivalent
chmod -R 775 storage bootstrap/cache public
Do not skip this A non-writable storage/ produces "The stream or file could not be opened" the moment Laravel boots — long before the installer can help.
Step 5 — Create the database
- cPanel → MySQL Databases.
- Create database
YOURUSER_leadhuband a dedicated user with a strong password. - Add the user to the database with ALL PRIVILEGES.
Expected result: you have four values ready for the next step: host (localhost), database name, username, password.
Bringing existing data instead of a fresh database? If you are moving hosts / restoring / installing pre-loaded data, you will import a dump rather than start empty — follow Database setup & migrations → Import an existing database, skip the browser installer in Step 6, and bring the source install's .env (its APP_KEY decrypts stored credentials — a new key cannot).
Full detail (including utf8mb4 and migration options): Database setup & migrations.
Step 6 — Run the browser installer
-
Open in a browser:
https://crm.zodiactechsoft.in/install.phpIf
storage/installed.lockdoes not exist yet, even hitting/bounces you here automatically. -
Step 1 — Requirements: PHP 8.4.1+, extensions, writable folders. Fix anything red.
-
Step 2 — Database: enter the credentials from Step 5 (host, port, database, user, password). The installer tests the connection before continuing.
-
Step 3 — Admin: create your first admin login (name, e-mail, password).
-
Step 4 — Install: review the summary — it shows the exact
APP_URLit will write to.env— and run the install. The installer writes.envand generatesAPP_KEY, migrates the schema, seeds base data (roles, plans, permissions, locales), creates the owner account, and writesstorage/installed.lock.
APP_URL is auto-derived — there is no URL prompt The installer writes APP_URL from the address you are using to reach it (scheme + host + subfolder, e.g. https://crm.example.com or http://localhost/Lead%20Hub). Before installing, confirm the summary line matches the final URL. Any domain, subdomain, or subfolder works — but if you later move the app, edit APP_URL in .env and run php artisan optimize:clear (a cached config keeps old URLs).
Expected result: the installer writes storage/installed.lock and points you at the login page.
The installer expects an EMPTY database It migrates the schema from scratch, seeds base data, and creates your first admin. If you instead imported a dump (existing tenants/leads/users), do not run the installer against it — it would create a second admin account on top of your data. For that path: copy .env.example to .env, set DB_* to the imported database, paste the source install's APP_KEY, then run php artisan optimize:clear && php artisan migrate --force (full steps).
Prefer the manual route? With Terminal: cp .env.example .env, edit the DB_* keys and APP_URL, then php artisan key:generate && php artisan migrate --force and create the first user in /super-admin. The installer simply automates these.
Step 7 — Layout check (the subfolder warning)
If the document root points at public/ — nothing to do. https://crm.zodiactechsoft.in/ serves the app.
If you could NOT point the document root at public/ (the app root itself is the web root, e.g. /home/USER/public_html/crm/), the shipped root files handle it:
- The root
.htaccessre-exports theAuthorizationheader, blocksapp|bootstrap|config|database|resources|routes|storageand dotfiles, forwards static trees frompublic/(build,css,js,storage,vendor,install.php, …), and routes everything else into the rootindex.php. - The root
index.phprequirespublic/index.phpin a way that keeps Symfony's base-URL detection aligned with/crm— so Laravel sees/login, not/crm/login.
This mirrors the local XAMPP setup exactly (http://localhost/Lead%20Hub/ works this way). Verify after install:
https://crm.zodiactechsoft.in/crm/login ← should load the login page
Subfolder + webhooks In the fallback layout every URL includes the subfolder — including API and webhook callback URLs you paste into Meta/WhatsApp (https://crm.zodiactechsoft.in/crm/webhook/...). If you can point the document root at public/, you avoid this class of mistakes entirely.
Step 8 — Common finishing commands (Terminal, optional)
cd ~/crm-app
php artisan storage:link # public link for uploaded files (see note)
php artisan migrate --force # if anything is pending
php artisan optimize:clear # clear stale compiled config/routes/views
storage:link is recommended but no longer load-bearing for the core branding flow: logo/favicon URLs are rewritten to the symlink-free /media route (app/Support/PublicMedia.php) and call recordings stream from the private disk through a signed route — neither needs the symlink. Uploaded files that still emit public /storage/... URLs (WhatsApp template media handles, Meta creative assets) do use it, so run it unless your host forbids symlinks entirely.
Step 8b — Behind Cloudflare / a proxy, and HTTPS cookie flags
Two .env tweaks the installer cannot infer (add them and run php artisan optimize:clear):
TRUSTED_PROXIES— if the site is behind Cloudflare or a reverse proxy (or any host that terminates TLS before PHP), setTRUSTED_PROXIES=*(or the proxy IPs, comma-separated). Without it$request->ip()is the proxy's IP — degrading login lockout, audit-log IPs, and the Super Admin IP allowlist.SESSION_SECURE_COOKIE=true— once the site is on HTTPS (any Let's Encrypt/cPanel SSL cert), set this so session cookies are only sent over TLS. Pair it withAPP_URL=https://...— anhttp://APP_URL on an HTTPS site breaks OAuth redirects and generated URLs.
Step 9 — Add the one cron entry
cPanel → Cron Jobs → add a once-per-minute job:
* * * * * cd /home/YOURUSER/crm-app && php artisan schedule:run >> /dev/null 2>&1
On hosts where the php binary is not the 8.4 one, use the full path (cPanel shows it, e.g. /usr/local/bin/php or ea-php84):
* * * * * /usr/local/bin/ea-php84 /home/YOURUSER/crm-app/artisan schedule:run >> /dev/null 2>&1
No SSH/Terminal at all? The product ships a fallback runner:
* * * * * php /home/YOURUSER/public_html/crm/cron.php
cron.php drains queued jobs (up to 30 per run, 55 s budget) and then runs schedule:run — see Cron & queue workers.
Expected result: within a couple of minutes, queued jobs start processing and scheduled tasks appear in logs.
Step 10 — First login and go-live checks
- Open
https://crm.zodiactechsoft.in/login(or/super-adminfor the platform panel) and sign in with the owner account. - Confirm:
/api/v1/healthreturns{"status":"ok",...}, the/docsportal loads, and a test e-mail sends from Settings → SMTP. - Work through the Go-live checklist before handing the system to anyone.
Common mistakes
- Uploading the project folder so that
public/is buried and pointing the document root at the app root without the shipped bridge files — the fallback layout only works with the shipped rootindex.php+.htaccesspresent. - Using
http://forAPP_URLon an HTTPS site — OAuth redirects and generated URLs break. - Running migrations via phpMyAdmin by hand instead of
artisan/installer — schema stays untracked. - Granting the DB user access to all databases instead of just the app's.
- Adding cron as
schedule:runonly and forgetting that queued jobs need the worker/cron drain too.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| 500 immediately on all pages with no install screen | storage/ not writable, or wrong PHP version |
| Installer loops back to step 1 | One requirement still red (usually an extension) |
| CSS/JS missing, page unstyled | Assets not published — run php artisan filament:upgrade and php artisan storage:link; check public/build exists |
Login redirects to /install.php |
storage/installed.lock missing (placeholder file got deleted) |
| API returns HTML 404 | mod_rewrite/.htaccess ignored — ask host to allow AllowOverride All |
| Cron seems dead | Wrong PHP path; test the command manually via Terminal |
Technical details
- Installer:
public/install.php(pure PHP; writes.env, migrates, creates owner, writesstorage/installed.lock). - Bridge files (subfolder layout): root
index.php, root.htaccess; the standard layout relies onpublic/index.php+public/.htaccessinstead. - Database schema:
database/migrations/; base data:database/seeders/DatabaseSeeder.php. - Diagnostics:
GET /api/v1/health.
Related guides
Was this page helpful?
Thanks — noted. ✦