CLI Commands Reference
FTP Deployer exposes two Artisan commands: ftp-deploy to execute the full deployment pipeline, and ftp-deploy:check to verify the FTP connection settings.
Command signature
php artisan ftp-deploy {profile=production} {--format=}
Arguments:
| Argument | Required | Default | Description |
|---|---|---|---|
profile |
no | production |
Profile key under config('ftp-deployer.profiles'). |
Options:
| Option | Required | Default | Description |
|---|---|---|---|
--format |
no | human text | Use --format=agent to print one structured JSON object for automation. |
Examples:
php artisan ftp-deploy
php artisan ftp-deploy production
php artisan ftp-deploy staging
php artisan ftp-deploy production --format=agent
What the command does
In order:
- Reads selected profile from
config/ftp-deployer.php. - Validates required FTP settings.
- Validates required path settings.
- Validates
paths.modeissimpleorversioned. - Validates versioned-only settings when versioned mode is enabled.
- Runs local
hooks.before_deploycommands. - Creates temp release directory.
- Copies deployable files while applying exclusions.
- Reuses local production vendor cache when Composer hashes match; otherwise runs production Composer install and saves cache.
- Removes runtime/cache files from temp release.
- Detects frontend builds and warnings.
- Builds local manifest.
- Connects to FTP/FTPS.
- Loads remote manifest from
.ftp-deployer/manifest.json. - Computes uploads, deletes, skipped files, and upload directories.
- In archive mode, creates
app-{random}.zipand deterministicvendor-{composer_json}-{composer_lock}.zipmetadata. - Uploads app archive every deploy.
- Uploads vendor archive only when the hash-named archive is missing remotely.
- In versioned mode, uploads managed public bootloader with the new release id hardcoded.
- Generates and uploads temporary HTTP runner.
- Calls runner via HTTP POST to extract app archive.
- Calls runner to extract vendor into the live app root or fresh release.
- Deletes removed managed files.
- Saves new remote manifest.
- Calls runner via HTTP POST with remote command list.
- Updates compatibility current pointer in versioned mode.
- Deletes runner and temporary app archive over FTP, best effort.
- Deletes local temp release directory.
- Runs local
hooks.after_deploycommands. - Prints summary with elapsed time.
Output
During deploy, progress lines show start time, temp paths, Composer hashes, archive reuse, runner phases, and final duration:
Starting FTP deploy [production] at 2026-07-02 21:08:55
Building temporary release
Composer vendor cache hit: /tmp/ftp-deployer-vendor-376a...-d4a...
Temporary release ready: /tmp/ftp-deployer-6ebf04d3dd3700e2
Composer inputs: composer.json=376a..., composer.lock=d4a...
Connecting FTP: ftp.example.com
Archive deploy: 89 changed file(s), 0 delete(s), 6 unchanged file(s).
Creating app archive temp: /tmp/app-714b....zip
Vendor archive name from composer hashes: vendor-376a...-d4a....zip
Uploading app.zip (323.1 KB) -> .ftp-deployer/archives/app-714b....zip
Reusing vendor.zip already on remote: .ftp-deployer/archives/vendor-376a...-d4a....zip
Uploading managed public bootloader for release 20260702210855
Temporary installer ready: install-3cf46566f3ae7312.php
Uploading install-3cf46566f3ae7312.php -> public_html/install-3cf46566f3ae7312.php
Calling install-3cf46566f3ae7312.php to extract 2 archive(s)
Calling install-3cf46566f3ae7312.php to run 4 remote command(s): migrate --force, optimize:clear, optimize, storage:link
Deploy complete in 12.34s. Uploaded 1, deleted 0, skipped 6.
Uploaded counts archives/files actually uploaded. A reused vendor archive does not increase this count.
Frontend warnings are printed as warning lines:
Warning: package.json found but no build manifest detected — did you run your build step?
Agent JSON output
Use --format=agent when scripts or AI agents need parseable output:
php artisan ftp-deploy production --format=agent
In agent format, progress messages and local hook output are buffered into logs; stdout receives one JSON object at the end.
Successful deploy example:
{
"status": "success",
"profile": "production",
"uploaded": 3,
"deleted": 1,
"skipped": 120,
"logs": [
{"level": "info", "source": "deploy", "message": "Uploading routes/web.php"}
],
"runner": {"ok": true, "logs": []}
}
Failed deploy example:
{
"status": "error",
"profile": "production",
"message": "Missing FTP setting: ftp.host",
"logs": [
{"level": "error", "source": "validation", "message": "Missing FTP setting: ftp.host"}
]
}
Exit codes
The command returns Laravel console status codes:
| Result | Exit code meaning |
|---|---|
| Deploy succeeds | Command::SUCCESS (0) |
| Deploy fails | Command::FAILURE (1) |
Failures are printed as one error message from the thrown exception.
Migration command (ftp-deploy:migrate)
Guides a user from simple deployment settings to versioned deployment settings for a selected profile.
Command signature
php artisan ftp-deploy:migrate {profile=production} {--mode=versioned} {--write} {--format=}
Arguments:
| Argument | Required | Default | Description |
|---|---|---|---|
profile |
no | production |
Profile key under config('ftp-deployer.profiles'). |
Options:
| Option | Required | Default | Description |
|---|---|---|---|
--mode |
no | versioned |
Target mode to migrate to. |
--write |
no | (plan only) | Update local .env with the generated configuration. |
--format |
no | human text | Use --format=agent to print one structured JSON object for automation. |
Output
The migration command will prompt for missing values (release root, shared root, and compatibility current pointer path) and run validation on the target profile configuration. It then shows a migration plan, environment variables block, and server-side instructions:
Migration Plan for profile [production]:
----------------------------------------
Target Mode: versioned
Proposed Path Settings:
- ftp_root: /public_html
- app_root: app
- public_root: public
- release_root: app/releases
- shared_root: app/shared
- current_path: app/current
WARNING: Remote .env and storage directory must be manually moved on your FTP server.
Required Server-Side Follow-up Steps:
1. Log into your FTP server.
2. Move remote .env from /public_html/app/.env to /public_html/app/shared/.env
3. Move remote storage/ from /public_html/app/storage to /public_html/app/shared/storage
4. Ensure write permissions (e.g., chmod -R 775) are set on the remote storage directory.
5. Run: php artisan ftp-deploy production
6. Confirm public/index.php contains the deployed release id and that the release contains `vendor/autoload.php`.
Agent JSON output
Use --format=agent when scripts or AI agents need parseable output:
Successful migration example:
{
"status": "success",
"profile": "production",
"logs": [
{"level": "info", "source": "migration", "message": "Migration plan generated successfully."}
],
"migration": {
"profile": "production",
"old_mode": "simple",
"new_mode": "versioned",
"paths": {
"release_root": "app/releases",
"shared_root": "app/shared",
"current_path": "app/current"
},
"written": false
}
}
Failed migration example:
{
"status": "error",
"profile": "production",
"message": "Missing path setting: paths.app_root",
"logs": [
{"level": "error", "source": "validation", "message": "Missing path setting: paths.app_root"}
]
}
Connection check command (ftp-deploy:check)
Verify FTP connection and credentials for a specified profile without performing any deployment operations.
Command signature
php artisan ftp-deploy:check {profile=production} {--format=}
Arguments:
| Argument | Required | Default | Description |
|---|---|---|---|
profile |
no | production |
Profile key under config('ftp-deployer.profiles'). |
Options:
| Option | Required | Default | Description |
|---|---|---|---|
--format |
no | human text | Use --format=agent to print one structured JSON object for automation. |
Output
During the check, progress and status are printed:
Connecting to FTP host...
FTP connection and login successful.
If it fails:
Connecting to FTP host...
FTP connection failed: FTP login failed.
Agent JSON output
Use --format=agent when scripts or AI agents need parseable output:
Successful check example:
{
"status": "success",
"profile": "production",
"message": "FTP connection and login successful.",
"logs": [
{"level": "info", "source": "connection", "message": "Attempting FTP connection..."},
{"level": "info", "source": "connection", "message": "FTP connection and login successful."}
]
}
Failed check example:
{
"status": "error",
"profile": "production",
"message": "FTP login failed.",
"logs": [
{"level": "info", "source": "connection", "message": "Attempting FTP connection..."},
{"level": "error", "source": "connection", "message": "FTP login failed."}
]
}
Exit codes
The command returns Laravel console status codes:
| Result | Exit code meaning |
|---|---|
| Connection succeeds | Command::SUCCESS (0) |
| Connection fails | Command::FAILURE (1) |
Profile validation errors
Missing profile
Deploy profile [staging] is missing.
Fix: add profiles.staging to config/ftp-deployer.php or use an existing profile.
Missing FTP setting
Missing FTP setting: ftp.host
Missing FTP setting: ftp.username
Missing FTP setting: ftp.password
Fix: set FTP_DEPLOYER_HOST, FTP_DEPLOYER_USERNAME, and FTP_DEPLOYER_PASSWORD or hardcode non-secret equivalents in config.
Missing path setting
Missing path setting: paths.ftp_root
Missing path setting: paths.app_root
Missing path setting: paths.public_root
Missing path setting: paths.app_url
Fix: configure required path keys.
Invalid mode
paths.mode must be simple or versioned.
Fix: use:
FTP_DEPLOYER_MODE=simple
or:
FTP_DEPLOYER_MODE=versioned
Missing versioned path
Missing versioned path setting: paths.release_root
Missing versioned path setting: paths.shared_root
Missing versioned path setting: paths.current_path
Fix: configure versioned paths.
Remote runner failures
The runner returns JSON. The CLI wraps failed runner responses as:
Runner failed: {"ok":false,"logs":[...]}
Common causes:
| Error | Cause | Fix |
|---|---|---|
Unauthorized |
Token missing or wrong. | Usually means stale/manual runner call; rerun deploy. |
Missing PHP extension: ... |
Remote PHP lacks required extension. | Enable extension in cPanel PHP settings. |
Missing required path: .../.env |
Remote .env missing. |
Upload/create .env on host. |
Remote .env must contain a non-empty APP_KEY. |
.env lacks app key. |
Set APP_KEY before deploy. |
Path is not writable: .../storage |
PHP cannot write storage. | Fix directory ownership/permissions. |
Command log with ok:false |
Artisan command failed. | Inspect sanitized output and fix app/config/database issue. |
Recommended command workflows
Local development deploy
No frontend:
php artisan ftp-deploy production
React/Vite with Bun:
bun install --frozen-lockfile
bun run build
php artisan ftp-deploy production
Or put build in before_deploy hook:
'hooks' => [
'before_deploy' => [
'bun install --frozen-lockfile',
'bun run build',
],
'after_deploy' => [],
],
Then run:
php artisan ftp-deploy production
CI deploy
Example GitHub Actions shape:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: ftp
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: composer install --no-interaction --prefer-dist
- run: npm ci
- run: npm run build
- run: php artisan ftp-deploy production
env:
FTP_DEPLOYER_HOST: ${{ secrets.FTP_DEPLOYER_HOST }}
FTP_DEPLOYER_USERNAME: ${{ secrets.FTP_DEPLOYER_USERNAME }}
FTP_DEPLOYER_PASSWORD: ${{ secrets.FTP_DEPLOYER_PASSWORD }}
FTP_DEPLOYER_APP_URL: https://laravelapp.inja.online
Remote command examples
Default Laravel deploy
'remote_commands' => [
'migrate --force',
'optimize:clear',
'optimize',
['command' => 'storage:link', 'ignore_failures' => true],
],
Safer first deploy without migrations
'remote_commands' => [
'optimize:clear',
'optimize',
['command' => 'storage:link', 'ignore_failures' => true],
],
Custom app setup
'remote_commands' => [
'migrate --force',
'db:seed --class=RequiredProductionSeeder --force',
'app:setup:cache',
'app:sync-permissions',
'optimize:clear',
'optimize',
],
See Cookbook & Examples for Bun/React, no-frontend, queue, cache, CI, and cPanel layout recipes.
Rollback in versioned mode
Versioned mode uses a managed public bootloader with the release id hardcoded. The deployer still writes {current_path} for compatibility, but the bootloader does not read it on each request.
To roll back manually, update {public_root}/index.php so the release line points to a previous release id:
$release = rtrim('app/releases', '/').'/20260701112233';
A safer rollback is to redeploy the desired code revision, letting the deployer regenerate the bootloader and manifest.
[!WARNING] This only rolls back PHP code routing through the bootloader. It does not roll back database migrations, queues, uploaded public assets, vendor archives, or external side effects.