Skip to main content

How to Deploy Files to a Remote Server via FTP in Batch Script

FTP (File Transfer Protocol) remains a common method for deploying files to web hosting providers, legacy systems, and environments where direct network share access is not available.

While Windows includes a built-in FTP client (ftp.exe) that can be driven via Batch Script, it has a major limitation: it only supports Active Mode FTP. Because most modern networks operate behind NAT (Network Address Translation) and firewalls, Active Mode data connections often hang or fail.

In this guide, we will explore how to automate file deployments using the native ftp.exe (for internal networks), as well as robust alternatives using PowerShell and WinSCP (for modern internet-facing deployments).

Method 1: Uploading a Single File (Native FTP)

The Windows FTP client at %SystemRoot%\System32\ftp.exe can be automated by passing it a text file containing FTP commands using the -s: switch.

@echo off
setlocal enabledelayedexpansion

set "ftp_host=ftp.example.com"
set "ftp_user=deployer"
:: Use a strong password. Delayed expansion protects special characters like <, >, &
set "ftp_pass=SecureP@ss<123&"
set "remote_dir=/public_html"
set "local_file=dist\index.html"

if not exist "%local_file%" (
echo [ERROR] Local file not found: %local_file%
pause
exit /b 1
)

echo Uploading %local_file% to %ftp_host%...

:: Create a temporary FTP command script
set "script=%temp%\ftp_upload_%random%.txt"

(
echo open !ftp_host!
echo !ftp_user!
echo !ftp_pass!
echo binary
echo cd !remote_dir!
echo put "!local_file!"
echo quit
) > "%script%"

:: Execute FTP script
:: -n: Suppresses auto-login prompts
:: -i: Disables interactive prompting during multiple file transfers
ftp -i -n -s:"%script%"

:: Clean up (IMPORTANT: The script contains your plain-text password)
del "%script%" 2>nul

echo [DONE] Upload complete.
pause

Method 2: Uploading an Entire Directory (Native FTP)

Instead of looping through files with Batch, you can use FTP's native lcd (Local Change Directory) and mput (Multiple Put) commands to upload an entire folder's contents efficiently.

@echo off
setlocal enabledelayedexpansion

set "ftp_host=ftp.example.com"
set "ftp_user=deployer"
set "ftp_pass=SecureP@ss123"
set "remote_dir=/public_html/app"
:: Absolute paths work best for lcd
set "local_dir=%CD%\dist"

if not exist "%local_dir%\" (
echo [ERROR] Local directory not found: %local_dir%
pause
exit /b 1
)

echo Deploying %local_dir% to %ftp_host%%remote_dir%...

set "script=%temp%\ftp_deploy_%random%.txt"

(
echo open !ftp_host!
echo !ftp_user!
echo !ftp_pass!
echo binary
echo cd !remote_dir!
echo lcd "!local_dir!"
echo mput *
echo quit
) > "%script%"

:: Execute (The -i flag is crucial here so mput doesn't prompt Y/N for every file)
ftp -i -n -s:"%script%"

del "%script%" 2>nul

echo [DONE] Directory upload complete.
pause
warning

The native mput * command is not recursive. It will only upload the files in the immediate root of %local_dir%. It will skip subdirectories entirely. If you need to deploy a complex folder structure, use Method 4.

If your native FTP script hangs after the mput or put command, you are likely hitting the Active Mode firewall/NAT wall. PowerShell uses Passive Mode by default, which safely navigates modern networks.

@echo off
setlocal

set "ftp_host=ftp://ftp.example.com"
set "ftp_user=deployer"
set "ftp_pass=SecureP@ss123"
set "remote_dir=/public_html"
set "local_dir=dist"

if not exist "%local_dir%\" (
echo [ERROR] Local directory not found.
pause
exit /b 1
)

echo Deploying via PowerShell (Passive Mode FTP^)...
echo.

powershell -NoProfile -Command ^
"$cred = New-Object System.Net.NetworkCredential('%ftp_user%', '%ftp_pass%');" ^
"$files = Get-ChildItem -Path '%local_dir%' -File;" ^
"if ($files.Count -eq 0) { Write-Host '[ERROR] No files found.'; exit 1 };" ^
"$webclient = New-Object System.Net.WebClient;" ^
"$webclient.Credentials = $cred;" ^
"foreach ($file in $files) {" ^
" $uri = '%ftp_host%%remote_dir%/' + $file.Name;" ^
" Write-Host ('Uploading {0}...' -f $file.Name) -NoNewline;" ^
" try {" ^
" $webclient.UploadFile($uri, $file.FullName) | Out-Null;" ^
" Write-Host ' [OK]' -ForegroundColor Green;" ^
" } catch {" ^
" Write-Host (' [FAIL] ' + $_.Exception.Message) -ForegroundColor Red;" ^
" }" ^
"}"

pause

Method 4: The WinSCP Way (Recursive Sync & SFTP)

Deploying a modern application usually involves deep directory trees (like a React build folder or a compiled .NET application). Writing custom Batch/FTP scripts to create remote directories dynamically is highly error-prone.

The industry standard for scriptable deployment on Windows is WinSCP. It supports recursion, synchronization (only uploading changed files), and secure protocols (SFTP/FTPS).

  1. Install WinSCP (winget install WinSCP).
  2. Use the WinSCP.com command-line executable.
@echo off
setlocal

set "winscp=%ProgramFiles(x86)%\WinSCP\WinSCP.com"

if not exist "%winscp%" (
echo [ERROR] WinSCP not found. Please install WinSCP first.
pause
exit /b 1
)

echo Starting secure deployment...

:: WinSCP command execution
:: Uses FTPS (TLS) by default, synchronizes directories, and supports passive mode
"%winscp%" /command ^
"open ftps://deployer:SecureP@ss123@ftp.example.com/ -certificate=""*""" ^
"synchronize remote dist/ /public_html/" ^
"exit"

if %errorlevel% equ 0 (
echo [SUCCESS] Synchronization complete.
) else (
echo [ERROR] Deployment failed. Exit code: %errorlevel%
)
pause
success

Why Synchronize? The synchronize remote command compares your local dist folder to the remote /public_html/ folder. It natively recreates all subdirectories, deletes old files on the server that no longer exist locally, and only uploads files that have changed. This transforms your batch script into a powerful deployment pipeline.

Common Mistakes

The Wrong Way: Echoing Passwords without Delayed Expansion

:: RISKY AND BUGGY
set "ftp_pass=P@ss&word"
echo %ftp_pass% >> script.txt

The Problem: Standard expansion evaluates the & character as a command separator. The script will try to execute word >> script.txt as a new command, breaking the FTP login. Always use echo !ftp_pass! with setlocal enabledelayedexpansion.

The Wrong Way: Assuming ftp.exe provides reliable exit codes

ftp -i -n -s:script.txt
if %errorlevel% equ 0 echo Success!

The Problem: The Windows ftp.exe client almost always returns an %errorlevel% of 0 to the Batch file, even if the login fails, the file doesn't exist, or the connection times out. If you need verified success metrics, you must either parse the output log or use PowerShell/WinSCP (which return proper exit codes).

The Wrong Way: Uploading in ASCII Mode

:: WRONG - Corrupts binary files
echo ascii >> script.txt
echo put myapp.zip >> script.txt

The Problem: ASCII mode converts line endings (\n to \r\n) during transfer, which permanently corrupts non-text files (images, compiled code, ZIPs). Always use binary mode for deployments. Binary mode transfers text files perfectly well too.

Best Practices

  1. Abandon ftp.exe for external deployments: Because of its lack of Passive Mode and encryption, native ftp.exe should only be used for internal, legacy intranet transfers.
  2. Always use binary mode: Prevents silent corruption of your deployed assets.
  3. Use the -i flag: If you use mput, failing to include -i will cause the script to pause indefinitely, waiting for a human to type Y to confirm the upload of every single file.
  4. Clean up script files: Never leave ftp_script.txt sitting in your %temp% directory. It contains your plain-text credentials. Use del "%script%" 2>nul immediately after execution.
  5. Move to SFTP/FTPS: Standard FTP transmits credentials and files in entirely plain text. Anyone monitoring the network can capture your server credentials. Use WinSCP or SSH for encrypted deployments.