Introduction
FTP Deployer deploys Laravel applications to shared cPanel-style hosting where you have FTP/FTPS and HTTPS, but no SSH.
The package runs from your local machine or CI. It prepares a production-ready release directory, uploads changed files over FTP, then calls a temporary token-protected HTTP runner on the target host to execute Laravel Artisan commands.
[!IMPORTANT] The package does not create or update the remote
.envfile. Place.envon the server before deploying, and make sure it contains a non-emptyAPP_KEY.
What problem this solves
Shared hosting usually blocks SSH, so normal Laravel deploy steps become manual:
- Upload files with an FTP client.
- Manually avoid overwriting
.env,storage, logs, sessions, and cache files. - Upload
vendor/because Composer cannot run remotely. - Visit some installer URL or click around cPanel to clear caches and run migrations.
- Hope stale
bootstrap/cache/*.phpfiles do not break the new code.
FTP Deployer turns that into one command:
php artisan ftp-deploy production
Current feature set
- One Artisan command:
ftp-deploy {profile=production}. - Named deploy profiles in
config/ftp-deployer.php. - FTP/FTPS upload using PHP
ext-ftp; the package currently also keepslazzard/php-ftp-clientas a dependency. - Temporary release directory outside the working tree.
- Production Composer install inside the temporary release directory.
- Exclusion matching for source-control, local dev,
.env, cache, log, session, and runtime paths. - Manifest-based file diffing using
.ftp-deployer/manifest.jsonon the remote host. - Special
vendor/handling driven bycomposer.jsonandcomposer.lockhashes. - Frontend build output detection for Vite, Vite 5+, Laravel Mix, and explicit named builds.
- Stale-build warnings when frontend input files change but build manifests do not.
- Versioned deployment mode with release directories, shared state, compatibility current pointer, hashed vendor cache, and managed public bootloader.
- Short-lived HTTP runner with random filename and random token.
- Runner requirement checks,
.envvalidation, cache invalidation, and remote Artisan command execution. - Runner cleanup over FTP after success or failure.
- Local shell hooks before and after deploy.
Architecture in one picture
Simple mode vs versioned mode
Simple mode
Simple mode is default. It maps local Laravel files into one remote app root and maps local public/ files into one public root.
Default layout:
{ftp_root}/app/ Laravel app root: artisan, vendor, bootstrap, storage
{ftp_root}/app/public/ public web root
Simple mode is best when your cPanel account points the domain directly at app/public or a similar public folder.
Versioned mode
Versioned mode uploads app code under release directories and keeps shared state outside releases.
Typical layout:
{ftp_root}/app/
current compatibility pointer written after deploy
shared/
.env
storage/
releases/
20260702123456/
artisan
vendor/
bootstrap/
public/
{ftp_root}/public_html/
index.php managed bootloader with hardcoded release id
build/ stable frontend assets
The managed public_html/index.php hardcodes the deployed release id, boots that release, and injects shared .env and storage paths. It does not read app/current on every request.
Versioned mode is useful because each deploy extracts into a fresh release while reusing hash-named vendor ZIP uploads. It is not full zero-downtime deployment: migrations and stable public assets can still affect live traffic.
What the package deliberately does not do
- No SSH deploys.
- No remote Composer install.
- No remote npm/yarn/pnpm/bun build.
- No cPanel API integration.
- No web dashboard.
- No
.envgeneration or secret transport through the runner payload. - No guaranteed zero downtime.
Next steps
- Read Installation.
- Configure a profile in Configuration.
- Learn the deploy flow in Core Concepts.
- Run the command from CLI Commands.
- Copy common setups from Cookbook & Examples.
- Customize behavior from Extending & Overriding.