Appearance
05_DEPLOY-ALL
Deploy-All.bat - Comprehensive Deployment Guide
Overview
This guide documents a Windows batch script deployment system that builds and deploys full-stack applications (PHP backend + JavaScript frontend) to production servers via FTP using curl. This approach is ideal for shared hosting environments where SSH access may be limited but FTP is available.
Architecture & Purpose
What This System Does
- Builds the frontend application (Vite/React/etc.)
- Uploads backend PHP files via FTP
- Uploads frontend build artifacts via FTP
- Manages environment-specific configurations
- Provides post-deployment checklist
Why Use This Approach
- No SSH Required: Works on shared hosting with only FTP access
- Windows-Friendly: Native batch script, no WSL or Git Bash needed
- Atomic Builds: Frontend built locally before upload ensures consistency
- Selective Upload: Only uploads necessary files, excludes dev dependencies
- Environment Safety: Manages separate dev/production environment files
Directory Structure Concept
Local Development Structure
project-root/
├── public/ # Local dev web root
│ ├── index.php # Entry point (local dev)
│ └── dist/ # Frontend build output (generated)
│ ├── index.html
│ └── assets/
│ ├── index-[hash].js
│ └── index-[hash].css
├── frontend/ # Frontend source
│ ├── src/
│ ├── package.json
│ └── vite.config.js
├── src/ # Backend source (PHP)
│ ├── Controllers/
│ ├── Models/
│ ├── Services/
│ └── Middleware/
├── config/ # Configuration files
├── database/ # Database migrations/seeds
├── bin/ # CLI scripts
├── vendor/ # Composer dependencies (NOT uploaded)
├── node_modules/ # NPM dependencies (NOT uploaded)
├── composer.json
├── .env.local # Local environment
├── .env.deploy-real # Production environment (uploaded as .env)
└── deploy-all.bat # This deployment scriptProduction Server Structure
CRITICAL: Production often has a FLAT structure, not nested in public/
/ops/ # Production root (web-accessible)
├── index.php # From local public/index.php
├── .htaccess # Production-specific (may differ from local)
├── .env # From local .env.deploy-real
├── dist/ # From local public/dist/
│ ├── index.html
│ └── assets/
├── src/ # Backend code
├── config/
├── database/
├── bin/
├── composer.json
└── vendor/ # Installed on server via SSH/cPanelKey Structural Differences
| Aspect | Local Development | Production |
|---|---|---|
| Entry Point | public/index.php | index.php (root) |
| Frontend Build | public/dist/ | dist/ (root) |
| Web Root | public/ subdirectory | Application root |
| .htaccess | References public/ | References current directory |
| Environment | .env.local | .env (from .env.deploy-real) |
Core Script Components
1. Configuration Section
batch
@echo off
setlocal enabledelayedexpansion
REM FTP Configuration
set FTP_HOST=ftp.example.com
set FTP_USER=username@example.com
set FTP_PASS=password
set FTP_URL=ftp://%FTP_HOST%Adaptation Notes:
- Set FTP credentials for your hosting provider
- Use environment variables or encrypted storage for sensitive credentials in production scripts
- Consider reading from a
.deploy-configfile instead of hardcoding
2. Pre-Deployment Safety Check
batch
echo WARNING: Deploying to PRODUCTION
echo Target: https://example.com/app/
echo.
set /p CONFIRM="Continue? (y/n): "
if /i not "%CONFIRM%"=="y" (
echo Deployment cancelled.
exit /b 1
)Why This Matters:
- Prevents accidental production deployments
- Gives developer moment to verify changes
- Can be bypassed with environment variable for CI/CD
3. Frontend Build Step
batch
echo ============================================
echo Step 1: Build Frontend
echo ============================================
if not exist "frontend" (
echo Error: frontend directory not found
exit /b 1
)
cd frontend
call npm run build
cd ..
if not exist "public\dist" (
echo Error: Build failed - public\dist not found
exit /b 1
)Key Points:
- Runs
npm run buildwhich triggers Vite/Webpack/etc. - Validates build output exists before uploading
- Fails fast if build fails (prevents uploading broken code)
- Build output location:
public/dist/
Adaptation:
- Change
frontenddirectory name if different - Adjust
public\distpath based on your build output location - Add additional build steps (TypeScript compilation, etc.)
4. Backend File Upload
batch
set "BASE_DIR=%CD%"
REM Upload src directory
echo Uploading src/...
for /r src %%f in (*.php) do (
set "fullpath=%%f"
set "relpath=!fullpath:%BASE_DIR%\=!"
set "relpath=!relpath:\=/!"
echo -^> !relpath!
curl -T "%%f" "%FTP_URL%/!relpath!" --user "%FTP_USER%:%FTP_PASS%" --ftp-create-dirs
if errorlevel 1 echo ERROR uploading !relpath!
)How This Works:
set "BASE_DIR=%CD%": Stores current directory for relative path calculationfor /r src %%f in (*.php): Recursively finds all .php files in src/- Path Conversion:
- Removes base directory from full path
- Converts Windows backslashes to FTP forward slashes
curl -T: Uploads file to FTP server--ftp-create-dirs: Automatically creates remote directories- Error Handling: Checks curl exit code and logs errors
Adaptation:
- Change file extension pattern (
*.php→*.py,*.js, etc.) - Add multiple loops for different file types
- Modify
srcto match your source directory
5. Special File Handling
Entry Point (index.php)
batch
REM Upload index.php to root (NOT in public/)
echo Uploading index.php to /ops root...
curl -T "public\index.php" "%FTP_URL%/index.php" --user "%FTP_USER%:%FTP_PASS%"Why Special?
- Local:
public/index.php(in subdirectory) - Production:
index.php(at root) - Needs explicit upload with path transformation
.htaccess Handling
batch
REM SKIP .htaccess upload - Production uses custom .htaccess
echo Skipping .htaccess upload (production has custom version)Critical Consideration:
- Local .htaccess may reference
public/subdirectory - Production .htaccess must reference current directory
- Often better to maintain separate production .htaccess on server
- Uploading wrong .htaccess can cause 500 errors
Recommendation:
- Keep
.htaccess.productionin version control - Manually upload once to production
- Skip in deployment script to avoid overwriting
Environment File
batch
REM Upload .env.deploy-real as .env
if exist ".env.deploy-real" (
echo Uploading .env.deploy-real -^> .env...
curl -T ".env.deploy-real" "%FTP_URL%/.env" --user "%FTP_USER%:%FTP_PASS%"
) else (
echo Warning: .env.deploy-real not found - skipping
)Environment Strategy:
.env.local→ Local development (not in version control).env.deploy-real→ Production config (in version control or secure vault)- Deployed as
.envon production server
Security Note:
- Never commit actual
.env.localwith secrets - Use
.env.deploy-realwith production values only - Consider using
.env.exampleas template
6. Frontend Build Upload
batch
echo ============================================
echo Step 3: Upload Frontend Build
echo ============================================
REM Upload dist files to /dist/ (ROOT, not public/)
echo -^> dist/index.html
curl -T "public\dist\index.html" "%FTP_URL%/dist/index.html" --user "%FTP_USER%:%FTP_PASS%" --ftp-create-dirs
REM Upload all assets
echo -^> Uploading assets...
for %%f in (public\dist\assets\*) do (
echo %%~nxf
curl -T "%%f" "%FTP_URL%/dist/assets/%%~nxf" --user "%FTP_USER%:%FTP_PASS%" --ftp-create-dirs
)Frontend Upload Pattern:
- Upload main HTML:
public/dist/index.html→/dist/index.html - Upload static assets:
public/dist/assets/*→/dist/assets/* - Optional files: favicon, vite.svg, etc.
Hash-Busting:
- Vite/Webpack generates files like
index-abc123.js - Each build creates new hashes
- Old files remain on server (safe for caching)
- Can add cleanup script to remove old hashes
7. Additional Directory Uploads
batch
if exist "config" (
echo Uploading config/...
for /r config %%f in (*.php) do (
REM ... same upload pattern as src/
)
)Pattern for Optional Directories:
- Check if directory exists
- Upload if present
- Skip gracefully if absent
- Useful for: config/, database/, bin/
Curl FTP Command Reference
Basic Upload
batch
curl -T "local-file.txt" "ftp://ftp.example.com/remote-file.txt" --user "username:password"Create Remote Directories
batch
curl -T "file.txt" "ftp://example.com/path/to/file.txt" --user "user:pass" --ftp-create-dirsError Handling
batch
curl -T "file.txt" "ftp://example.com/file.txt" --user "user:pass"
if errorlevel 1 (
echo ERROR: Upload failed
exit /b 1
)Useful Curl Options
| Option | Purpose |
|---|---|
-T <file> | Upload file |
--user "user:pass" | FTP authentication |
--ftp-create-dirs | Create remote directories |
-v | Verbose output (debugging) |
--retry 3 | Retry failed uploads |
--speed-limit 1000 | Minimum speed (bytes/sec) |
--max-time 300 | Timeout in seconds |
Post-Deployment Steps
1. Install Server Dependencies
bash
# SSH into server
ssh user@server
# Navigate to application directory
cd /path/to/app
# Install composer dependencies
composer install --no-dev --optimize-autoloaderWhy Not Upload vendor/?
- Large directory (50-200MB+)
- Slow FTP upload
- Better to install directly on server
--no-devexcludes development dependencies--optimize-autoloaderimproves performance
2. Server Configuration
PHP-FPM Restart (for .env changes):
bash
sudo systemctl restart php-fpm
sudo systemctl restart apache2Or via cPanel:
- MultiPHP Manager → Restart PHP-FPM
3. Verification Checklist
batch
echo POST-DEPLOYMENT CHECKLIST:
echo [VERIFY] Test deployment:
echo 1. Health check: https://example.com/app/health
echo 2. Login test: https://example.com/app/
echo 3. Check functionality: [key features]Files That Should NEVER Be Uploaded
Automatically Excluded (Not in Upload Loops)
vendor/- Composer dependenciesnode_modules/- NPM dependencies.git/- Version controltests/- Test files.env.local- Local environment*.log- Log filescache/- Cache directorystorage/- Local storage
Explicit Exclusions in Script
batch
REM Do not upload these patterns
for /r src %%f in (*.php) do (
set "filename=%%~nxf"
REM Skip test files
echo !filename! | findstr /i "test.php" >nul
if not errorlevel 1 goto :skip
REM Upload
curl -T "%%f" ...
:skip
)Error Handling & Logging
Basic Error Detection
batch
curl -T "file.txt" "%FTP_URL%/file.txt" --user "%FTP_USER%:%FTP_PASS%"
if errorlevel 1 (
echo ERROR uploading file.txt
REM Continue or exit
)Enhanced Error Logging
batch
set ERROR_COUNT=0
curl -T "file.txt" "%FTP_URL%/file.txt" --user "%FTP_USER%:%FTP_PASS%"
if errorlevel 1 (
set /a ERROR_COUNT+=1
echo [ERROR] file.txt >> deploy.log
)
if %ERROR_COUNT% gtr 0 (
echo Deployment completed with %ERROR_COUNT% errors
exit /b 1
)Advanced Features & Optimizations
1. Parallel Uploads (Experimental)
batch
REM Start multiple curl processes in background
start /b curl -T "file1.txt" "%FTP_URL%/file1.txt" --user "%FTP_USER%:%FTP_PASS%"
start /b curl -T "file2.txt" "%FTP_URL%/file2.txt" --user "%FTP_USER%:%FTP_PASS%"
REM Wait for all to complete (requires additional logic)2. Delta Deployments (Only Changed Files)
batch
REM Compare file dates
for %%f in (src\*.php) do (
REM Use PowerShell to compare timestamps
powershell -Command "if ((Get-Item '%%f').LastWriteTime -gt (Get-Date).AddHours(-24)) { exit 0 } else { exit 1 }"
if not errorlevel 1 (
curl -T "%%f" ...
)
)3. Deployment Rollback Support
batch
REM Before deployment, create backup on server
curl -T ".deployment-id" "%FTP_URL%/.deployment-id" --user "%FTP_USER%:%FTP_PASS%"
REM Store deployment timestamp
echo %DATE%-%TIME% > .deployment-id4. Configuration File Management
Instead of hardcoding credentials:
batch
REM Read from .deploy-config file
for /f "tokens=1,2 delims==" %%a in (.deploy-config) do (
if "%%a"=="FTP_HOST" set FTP_HOST=%%b
if "%%a"=="FTP_USER" set FTP_USER=%%b
if "%%a"=="FTP_PASS" set FTP_PASS=%%b
)Adapting for Your Project
Step 1: Identify Your Stack
Frontend:
- [ ] Build tool (Vite, Webpack, Parcel, etc.)
- [ ] Output directory (
dist/,build/,public/) - [ ] Build command (
npm run build)
Backend:
- [ ] Language (PHP, Python, Node.js, etc.)
- [ ] File extensions to upload
- [ ] Directory structure
Step 2: Map Directory Structures
Local Structure:
[Your local web root directory]
[Your source code directory]
[Your build output directory]Production Structure:
[Production web root]
[Production source location]
[Production static assets location]Step 3: Customize Upload Loops
batch
REM Template for file uploads
for /r [SOURCE_DIR] %%f in ([*.extension]) do (
set "fullpath=%%f"
set "relpath=!fullpath:%BASE_DIR%\=!"
set "relpath=!relpath:\=/!"
curl -T "%%f" "%FTP_URL%/[TARGET_PATH]/!relpath!" --user "%FTP_USER%:%FTP_PASS%" --ftp-create-dirs
)Step 4: Add Project-Specific Steps
Before Upload:
- [ ] Run tests
- [ ] Build documentation
- [ ] Minify/optimize assets
- [ ] Generate sitemap
After Upload:
- [ ] Clear CDN cache
- [ ] Warm up application cache
- [ ] Send deployment notification
- [ ] Create deployment tag in Git
Security Considerations
1. Credential Management
DON'T:
batch
REM ❌ Hardcoded passwords in script
set FTP_PASS=mypassword123DO:
batch
REM ✅ Read from environment variable
set FTP_PASS=%DEPLOY_FTP_PASS%
REM ✅ Or prompt for password
set /p FTP_PASS="Enter FTP password: "2. Environment Files
- Never commit
.env.localor.env.deploy-realwith actual secrets - Use
.env.exampleas template - Store production secrets in password manager or CI/CD vault
3. FTP vs SFTP
Current script uses FTP:
- Simple, widely supported
- Credentials sent in plain text
- Consider using SFTP for better security:
batch
REM SFTP upload (requires sftp.exe or psftp.exe)
echo put local-file.txt remote-file.txt | sftp -b - user@host4. Deployment Verification
Always include health check endpoints:
php
// health.php
echo json_encode([
'status' => 'ok',
'timestamp' => time(),
'version' => '1.0.0'
]);Common Issues & Solutions
Issue 1: "No such file or directory" on Server
Cause: Remote directories don't exist
Solution: Add --ftp-create-dirs to curl command
Issue 2: 500 Server Error After Deployment
Causes:
- Wrong .htaccess file uploaded
- PHP syntax error
- Missing .env file
- Incorrect file permissions
Solution:
- Check server error logs
- Verify .htaccess references correct paths
- Test PHP files locally first
- SSH in and check file permissions
Issue 3: Frontend Shows Old Version
Causes:
- Browser cache
- CDN cache
- Old files still on server
Solutions:
- Hard refresh (Ctrl+Shift+R)
- Clear CDN cache
- Add cache-busting query params
- Update version in HTML
Issue 4: Upload Times Out for Large Files
Solution: Add timeout and retry options
batch
curl -T "large-file.zip" "%FTP_URL%/file.zip" --user "%FTP_USER%:%FTP_PASS%" --max-time 600 --retry 3Performance Benchmarks
Typical deployment times:
| Component | Files | Size | Time (FTP) |
|---|---|---|---|
| Backend (src/) | 50 PHP files | 2MB | 30-60s |
| Frontend (dist/) | 10-20 assets | 5MB | 60-120s |
| Config/Database | 10 files | 500KB | 10-20s |
| Total | ~80 files | ~8MB | 2-4 min |
Optimization Strategies:
- Compress assets before upload
- Use delta deployments (only changed files)
- Parallel uploads for independent files
- Upload to CDN separately for static assets
Alternative Deployment Methods
1. SFTP (More Secure)
batch
echo put local.txt remote.txt | sftp -b - user@host2. rsync (Requires WSL or Git Bash)
bash
rsync -avz --exclude 'vendor' --exclude 'node_modules' ./ user@host:/path/3. Git Deployment
bash
git push production mainRequires post-receive hook on server:
bash
#!/bin/bash
cd /var/www/app
git checkout -f
composer install --no-dev
npm run build4. CI/CD Pipeline
GitHub Actions:
yaml
- name: Deploy via FTP
uses: SamKirkland/FTP-Deploy-Action@4.3.0
with:
server: ftp.example.com
username: ${{ secrets.FTP_USER }}
password: ${{ secrets.FTP_PASS }}Checklist: Creating Your deploy-all.bat
- [ ] Configure FTP credentials (host, user, password)
- [ ] Set correct production URL for confirmation message
- [ ] Add frontend build command (npm run build, etc.)
- [ ] Map local → production directory structure
- [ ] Add upload loops for each source directory
- [ ] Handle special files (index.php, .htaccess, .env)
- [ ] Upload frontend build output
- [ ] Add error handling for critical uploads
- [ ] Include post-deployment checklist
- [ ] Test deployment on staging environment first
- [ ] Document any manual post-deployment steps
- [ ] Add deployment verification steps
Conclusion
This deployment approach provides a reliable, Windows-native solution for deploying full-stack applications to shared hosting environments. While not as sophisticated as modern CI/CD pipelines, it offers:
- Simplicity: No complex tooling required
- Reliability: Direct file upload, easy to debug
- Flexibility: Easy to customize for specific needs
- Accessibility: Works anywhere curl is available
For production applications, consider graduating to:
- Git-based deployments
- CI/CD pipelines (GitHub Actions, GitLab CI)
- Container orchestration (Docker, Kubernetes)
- Platform-as-a-Service (Heroku, Vercel, Netlify)
But for getting started or working within constraints, deploy-all.bat is a powerful tool in your deployment toolkit.