Deploy only what changed. A fast, git-diff-powered CLI tool that syncs your local git project to a remote server via FTP, FTPS, or SFTP — no CI/CD pipeline required.
Most FTP/SFTP deploy tools re-upload your entire project on every run. local-git-deploy uses git diff to detect only the files that changed since your last deployment, making it:
- ⚡ Fast — uploads only changed files, not the whole project
- 🔐 Secure — keeps passwords and private keys out of your repo
- 🧹 Clean — automatically deletes removed or renamed files on the remote server
- 🖥️ Local-first — works from your machine, no GitHub Actions or CI server needed
- 🏠 Shared hosting friendly — perfect for cPanel, FTP-only hosts, and PHP projects
Install globally via npm:
npm install -g local-git-deployIn the root of your git project, create a local-git-deploy.yml (or .json) configuration file:
server: ftp.your-server.com
user: your_username
protocol: ftp # Options: ftp | ftps | sftp
port: 21 # 21 for FTP/FTPS, 22 for SFTP
remote_dir: /public_html
exclude: # Glob patterns to exclude from deployment
- ".env"
- "local-git-deploy.yml"
- "node_modules/**"
- ".git/**"Security Best Practice: Never put passwords in your YAML config. Use a
.envfile instead (and make sure.envis in your.gitignore).
# FTP / FTPS password
DEPLOY_PASSWORD=your_super_secret_password
# SFTP private key (optional)
DEPLOY_PRIVATE_KEY_PATH=/path/to/private/key.pemTip: If your SFTP server requires both a private key and a passphrase, provide both DEPLOY_PASSWORD and DEPLOY_PRIVATE_KEY_PATH. The CLI handles both automatically.
If you already have SSH key auth set up with your server, you may not need any credentials in your config at all. The tool automatically checks:
~/.ssh/config— resolves host aliases, user, port, and identity files- SSH Agent — uses keys loaded via
ssh-add(works on Linux, macOS, and Windows OpenSSH) - Default key paths — probes
~/.ssh/id_ed25519,~/.ssh/id_rsa,~/.ssh/id_ecdsa
Minimal config — no .env needed:
server: myserver # can be an alias from ~/.ssh/config
protocol: sftp
remote_dir: /var/www/appIf your ~/.ssh/config has this entry:
Host myserver
HostName actual-server.com
User deploy
IdentityFile ~/.ssh/my_deploy_key
Then local-git-deploy will automatically resolve the hostname, user, and key — just like ssh myserver or VS Code Remote SSH.
Priority: Explicit credentials (
.envor YAML) always override auto-detected ones. Zero-config only activates as a fallback.
Encrypted keys: If your auto-detected key is passphrase-protected, set
DEPLOY_PASSWORDin your.envas the passphrase, or load the key into your SSH agent withssh-add.
Commit your changes locally, then run:
local-git-deployThe CLI will:
- Connect to your remote server
- Read the remote
.deploy-sync-statefile to find the last deployed commit - Run
git diffbetween the remote commit and your localHEAD - Upload only new and modified files
- Delete files that were removed or renamed
- Update the remote state file with the new commit hash
local-git-deploy [options]
Options:
-c, --config <path> Path to config file (default: "local-git-deploy.yml")
-b, --branch <name> Override branch detection (for CI/CD)
-f, --force Deploy even if branch is not mapped in config
-V, --version Output version number
-h, --help Display help
Run scripts or commands on the remote server before and/or after file sync. This is useful for tasks like putting a site into maintenance mode, running database migrations, clearing caches, or restarting services.
Note: Deploy hooks require
protocol: sftpsince FTP/FTPS do not support remote command execution.
server: example.com
user: deploy
protocol: sftp
remote_dir: /var/www/app
pre_deploy:
- "cd /var/www/app && php artisan down"
post_deploy:
- "cd /var/www/app && composer install --no-dev"
- "cd /var/www/app && php artisan migrate --force"
- "cd /var/www/app && php artisan cache:clear"
- "cd /var/www/app && php artisan up"For complex multi-line logic, reference a local script file. The tool will upload it to a temp location on the server, execute it, then clean up.
pre_deploy:
- script: ./scripts/backup-db.sh
post_deploy:
- "cd /var/www/app && composer install --no-dev"
- script: ./scripts/post-deploy.sh- If any hook fails (non-zero exit code), the deployment is aborted.
- The remote state file is not updated on failure — re-running
local-git-deploywill re-attempt everything. - stdout is streamed in real-time to your terminal; stderr is highlighted in red.
Deploy different branches to different servers or directories — like GitHub Actions environments, but for FTP/SFTP.
Use the branches: key in your config. Top-level settings act as defaults, and each branch can override any setting — including server, user, protocol, port, and credentials.
server: example.com
user: deploy
protocol: sftp
branches:
main:
remote_dir: /var/www/production
password_env: DEPLOY_PASSWORD_PROD
post_deploy:
- "cd /var/www/production && php artisan migrate --force"
- "cd /var/www/production && php artisan cache:clear"
staging:
remote_dir: /var/www/staging
password_env: DEPLOY_PASSWORD_STAGING
post_deploy:
- "cd /var/www/staging && php artisan migrate --force"
develop:
server: dev.example.com # different server entirely
user: dev-deploy # different user
protocol: ftp # different protocol
port: 2121
remote_dir: /public_html/dev
password_env: DEPLOY_PASSWORD_DEV
"feature/*": # glob pattern for feature branches
remote_dir: /var/www/previewEach branch can read its password from a different environment variable using password_env:
branches:
main:
password_env: DEPLOY_PASSWORD_PROD # reads DEPLOY_PASSWORD_PROD from .env
staging:
password_env: DEPLOY_PASSWORD_STAGING # reads DEPLOY_PASSWORD_STAGING from .env# .env
DEPLOY_PASSWORD_PROD=super_secret_prod
DEPLOY_PASSWORD_STAGING=staging_passIf no password_env is specified, the default DEPLOY_PASSWORD is used.
The tool auto-detects the current git branch. You can override this with:
# Override branch (useful in CI/CD with detached HEAD)
local-git-deploy --branch staging
# Deploy an unmapped branch using base config defaults
local-git-deploy --forceEach branch gets its own state file on the remote server (e.g., .deploy-sync-state-main, .deploy-sync-state-staging). This prevents deploying from one branch from corrupting another branch's deployment state.
| Protocol | Port | Use Case |
|---|---|---|
ftp |
21 | Basic shared hosting |
ftps |
21 | FTP with TLS/SSL (cPanel, etc.) |
sftp |
22 | SSH-based secure file transfer + deploy hooks |
- Git-diff powered deploys — only changed files are transferred
- State tracking —
.deploy-sync-statefile on the server tracks the last deployed commit - Deletion & rename handling — automatically cleans up removed or renamed files on the remote
- Pre/post-deploy hooks — run commands or scripts on the server (SFTP only)
- Multi-branch deployment — deploy different branches to different servers/directories
- Per-branch credentials — each branch can use its own password via
password_env - Glob branch matching — use patterns like
feature/*to match dynamic branches - Glob exclude patterns — skip files like
node_modules,.env, config files - Secure credential handling — passwords and private keys loaded from
.env - SFTP private key support — supports passphrase-protected keys and two-factor SFTP servers
- Connection timeouts — prevents hanging on unreachable servers
- Self-signed cert support — opt-in with
insecure: truefor FTPS on dev servers - Resume-safe — if a deploy fails midway, run again and it picks up safely
- Shared Hosting Deployments — Deploy to cPanel, HostGator, GoDaddy, or any FTP/FTPS host.
- PHP & Node.js Projects — Ideal for Laravel, WordPress, CodeIgniter, Express, or custom apps.
- CI/CD Alternative — Get automated, branch-aware, incremental deploys without configuring GitHub Actions, GitLab CI, or Jenkins.
- Post-Deploy Automation — Run SSH hooks for database migrations, cache clearing, or restarting services automatically after a sync.
- Static Site Hosting — Quickly deploy HTML/CSS/JS bundles, React/Vue build folders, or static site generators.
Deployment failed midway?
No problem. The .deploy-sync-state only updates on a successful deploy. Just fix the issue and re-run local-git-deploy — it will re-attempt only the affected files.
Getting a "bad object" error?
This usually means the state file on the server has a stale or invalid commit hash. Delete the .deploy-sync-state file from your remote server and re-run to do a clean sync.
Filenames with spaces not working?
Make sure you are on v1.1.1 or later. Special character support was added in that release.
Deploy hooks not running?
Hooks require protocol: sftp. If you're using ftp or ftps, the tool will print a warning and skip hooks.
Branch not configured?
If you see "Branch X is not configured for deployment", either add your branch to the branches: config or use --force to deploy with base defaults.
Pull requests and issues are welcome! Please open an issue first to discuss any major changes.
pablo-codes — GitHub