Skip to content

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

  1. Builds the frontend application (Vite/React/etc.)
  2. Uploads backend PHP files via FTP
  3. Uploads frontend build artifacts via FTP
  4. Manages environment-specific configurations
  5. 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 script

Production 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/cPanel

Key Structural Differences

AspectLocal DevelopmentProduction
Entry Pointpublic/index.phpindex.php (root)
Frontend Buildpublic/dist/dist/ (root)
Web Rootpublic/ subdirectoryApplication root
.htaccessReferences 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-config file 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 build which 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 frontend directory name if different
  • Adjust public\dist path 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:

  1. set "BASE_DIR=%CD%": Stores current directory for relative path calculation
  2. for /r src %%f in (*.php): Recursively finds all .php files in src/
  3. Path Conversion:
    • Removes base directory from full path
    • Converts Windows backslashes to FTP forward slashes
  4. curl -T: Uploads file to FTP server
  5. --ftp-create-dirs: Automatically creates remote directories
  6. 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 src to 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.production in 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 .env on production server

Security Note:

  • Never commit actual .env.local with secrets
  • Use .env.deploy-real with production values only
  • Consider using .env.example as 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:

  1. Upload main HTML: public/dist/index.html/dist/index.html
  2. Upload static assets: public/dist/assets/*/dist/assets/*
  3. 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-dirs

Error 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

OptionPurpose
-T <file>Upload file
--user "user:pass"FTP authentication
--ftp-create-dirsCreate remote directories
-vVerbose output (debugging)
--retry 3Retry failed uploads
--speed-limit 1000Minimum speed (bytes/sec)
--max-time 300Timeout 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-autoloader

Why Not Upload vendor/?

  • Large directory (50-200MB+)
  • Slow FTP upload
  • Better to install directly on server
  • --no-dev excludes development dependencies
  • --optimize-autoloader improves performance

2. Server Configuration

PHP-FPM Restart (for .env changes):

bash
sudo systemctl restart php-fpm
sudo systemctl restart apache2

Or 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 dependencies
  • node_modules/ - NPM dependencies
  • .git/ - Version control
  • tests/ - Test files
  • .env.local - Local environment
  • *.log - Log files
  • cache/ - Cache directory
  • storage/ - 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-id

4. 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=mypassword123

DO:

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.local or .env.deploy-real with actual secrets
  • Use .env.example as 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@host

4. 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 3

Performance Benchmarks

Typical deployment times:

ComponentFilesSizeTime (FTP)
Backend (src/)50 PHP files2MB30-60s
Frontend (dist/)10-20 assets5MB60-120s
Config/Database10 files500KB10-20s
Total~80 files~8MB2-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@host

2. rsync (Requires WSL or Git Bash)

bash
rsync -avz --exclude 'vendor' --exclude 'node_modules' ./ user@host:/path/

3. Git Deployment

bash
git push production main

Requires post-receive hook on server:

bash
#!/bin/bash
cd /var/www/app
git checkout -f
composer install --no-dev
npm run build

4. 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.

lock

Enter PIN to continue