Skip to content
| Marketplace
Sign in
Visual Studio Code>Azure>FTP Deploy & SyncNew to Visual Studio Code? Get it now.
FTP Deploy & Sync

FTP Deploy & Sync

Akshay Landage

|
33 installs
| (0) | Free
Deploy and sync files over FTP, FTPS, SFTP — or straight to a local folder, network share or IIS — and apply the database scripts that go with them. Server profiles, one-click rule pipelines, versioned SQL migrations, incremental transfer, dry-run preview and timestamped backups.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

FTP Deploy

Incremental FTP/FTPS/SFTP deploy for VS Code, built around multiple servers as a first-class concern.

A rule running end to end: build, back up the server, delete the old files, upload the new ones

  • Profiles for every server you publish to, each with its own credentials, paths and file selection — switch or right-click to deploy to any of them.
  • Rules: a named pipeline of steps — run a build, back the server up, delete, upload — that runs end to end with one click.
  • Incremental by hash, not by timestamp, so only what genuinely changed goes over the wire.
  • Dry run everything first: the full plan, file by file, before anything moves.
  • Atomic uploads, size verification, and a manifest written per file so an interrupted run resumes cleanly.
  • Passwords and passphrases live in the OS keychain, never in the config.

Getting started

Nothing needs to be hand-written.

  1. Open the FTP Deploy icon in the activity bar.
  2. Click + New profile — a form covers every setting: protocol, host, port, credentials, key file, local and remote roots, include/exclude globs, and the mirror-delete switch.
  3. Test connection inside the form, before saving.
  4. Save — the profile is written to .vscode/ftp-deploy.json and the password goes to the OS keychain.
  5. Right-click the profile → Dry Run, then Deploy.

Repeat step 2 for every server. Right-click any profile to deploy to it directly, regardless of which one is active.

The config file stays readable and hand-editable — the editor writes through jsonc-parser, so your comments and formatting survive.

Profiles

A profile is one server. Names must be unique, because the name is the key for two separate things:

  • The stored credential. Namespaced by scope and profile name, so production in this project never reads production from another.
  • The manifest section. Each profile gets its own block of upload state. Two servers hold different state; a shared section would make a diff for one host reflect uploads made to another.

Mark a profile development, staging or production and the environment colours its badge everywhere it appears — the confirmation before a production deploy is the one you want to actually read.

Commands resolve their target in this order: right-click → active profile → defaultProfile → first profile.

Rules

A rule is a named pipeline: pick the files once, then list the steps to run on them. Running the rule executes every step in order with no further prompting — that is the whole point of it.

Add one from a profile → New rule. The editor has three parts: where it deploys, which files it acts on, and what it does to them.

Choosing the files

One selection per rule, applied to whichever side each step reads from.

  • Include files — the local folder as a tree, with the ticks showing what the globs really match, evaluated by the same matcher the deploy uses.
  • Exclude patterns — the raw glob lists, for anything a tree cannot express.
  • Server files — the live remote tree, browsed on demand. Ticking here edits the same lists. The bin icon deletes on the server immediately and is not part of the rule.

Every list carries the same toolbar: filter by name, Expand all, Collapse, Reload, Select all, Unselect all.

Ticking a folder writes folder/**; unticking one file adds just that path. Content-hashed names are generalised for you — untick main.1b26a9b7.js and the rule stores main.*.js, which still matches after the next build.

Steps

Step What it does
Run a command A shell command — your build — with its own working folder.
Back up the database A rollback point before the scripts run. Skips itself when nothing is pending.
Apply database scripts The SQL scripts this database has not had yet, in version order, once each.
Back up Timestamped copy of the server files about to be replaced, saved to your disk, with a MANIFEST.txt receipt.
Upload Only files that changed (manifest diff) or Every matched file, always (override).
Delete remote files Only what this run backed up, or everything the selection matches.
Delete local files Removes the matched files from your disk.
Mirror local Makes the server a copy of the local folder: deletes every server file the rule selects that is not in the local folder. Excludes keep server-only files (e.g. uploads/**). Skipped if an earlier step failed; refused if the local folder is empty.

Each step says what it will cost before you run it — an upload with no backup step above it says so, and a delete paired with a backup says that the server only loses files already saved to your disk.

Any step can narrow the rule's selection further under Files for this step: a folder tree of exactly what that step will touch, where unticking affects that step alone. "Back up everything, then delete all of it except web.config" is two ticks.

Cleanup steps that always run

Any step except Delete local files can be marked Always run this step, even if an earlier one fails. Marked steps move to the end of the rule and run there whatever happened — success, failure, or a run you cancelled.

This exists for pipelines that take something offline before they work. On IIS, a running app pool holds a lock on the DLLs you are about to replace, so the usual shape is:

Step
1. Upload app_offline.htm — IIS unloads the app and releases the locks
2. Back up the files about to be replaced
3. Upload the new build
4. Delete remote app_offline.htm — always

Without step 4 marked, a failed upload in step 3 stops the rule and leaves the site showing the offline page until somebody notices. With it marked, the site comes back whether the deploy worked or not.

The same shape covers anything that has to be undone: a maintenance flag, a firewall rule opened by a command step, a service stopped before a copy.

  • Cleanup steps run in the order written, after everything else.
  • One failing cleanup step does not stop the next.
  • Cancelling a run still runs them — that is the case they matter most in.
  • Delete local files cannot be marked. It would delete your files after the run that was supposed to upload them had already failed, which is the one thing this extension will not do.

Presets

One click each: Upload changed · Upload with override · Back up server · Back up then upload · Back up, delete, upload · Build, back up, delete, upload · Back up local then delete local · Upload + mirror local.

One rule for many targets

When the same rule repeats across servers, sites or apps, write it once and let it expand. A rule with a forEach block is a template: it produces one rule per item, substituting ${var} anywhere in the rule.

{
  "lists": {
    "sites": ["acme", "globex", "initech"]
  },

  "rules": [
    {
      "name": "${site} — UI",
      "forEach": { "site": "sites" },
      "profile": "web01",
      "localPath": "builds/${site}/ui",
      "remotePath": "/${site}/UI",
      "actions": [
        { "type": "backup", "source": "remote" },
        { "type": "upload", "overwrite": "ifChanged" }
      ]
    }
  ]
}

That is three rules, and adding a fourth site is one word. More to the point, changing a step changes it everywhere at once — the reason to template is maintenance, not typing.

Lists are named by you and hold either plain strings or objects. Objects carry more than one field per item:

"lists": {
  "sites": [
    { "name": "acme",   "path": "/acme/wwwroot",   "build": "acme-ui" },
    { "name": "globex", "path": "/globex/wwwroot", "build": "globex-ui" }
  ]
}

…addressed as ${site.name}, ${site.path}, ${site.build}.

Several lists at once produce every combination, so a rule can cover a grid of sites and apps from one definition:

"forEach": { "site": "sites", "app": ["ui", "api"] }

Three sites × two apps is six rules. A forEach may also name an inline array directly, as app does there, when the list is short and used once.

Substitution reaches every string in the rule — name, paths, globs, and every field of every step, commands included. A few rules keep it predictable:

  • A rule without forEach is never touched. Existing configs keep their ${...} strings exactly as written.
  • An unknown variable is an error, not an empty string. A typo cannot quietly produce a remote path with a missing folder in it.
  • $${VAR} passes through as ${VAR}, for shell variables inside a command step that the template must not consume.
  • The name must vary, or the template is refused — two rules cannot share a name.
  • Expansion is capped at 1000 rules, so a runaway grid fails loudly.

Generated rules appear everywhere ordinary ones do: the dashboard, Run Rule, Dry Run, Run All, and the tick boxes. Saving an edit to one is refused, and the refusal names the template it came from: there is one definition behind all of them, so the template is where a change belongs. Edit it and every rule it produces follows. That is the point of writing it once.

Building one without touching the config file

The rule editor opens with Repeat for each. Tick it and the rule becomes a template:

  • Name the variable — site, app, whatever reads well.
  • Fill the table. One row per rule you want. Start with a single column of names; press + on the header to add fields when a site differs in more than one way — a project folder called Pva.UI but a backup folder called PVA needs two.
  • Never type the syntax. In any field, type $ and a list of the variables appears — keep typing to filter, Enter to insert. There is a {} button beside every field that does the same, and the chips above the table drop a token into whichever field the cursor was last in. If a {site} or $site does slip through, the card says which field and offers to correct it — but never a bare word, because a folder is allowed to be called site.
  • Watch the preview. Underneath: "Produces 6 rules: TOPS-Taxpayer-UI-Deploy, TOPS-PVA-UI-Deploy, …". It is expanded by the same code the loader uses, so what it lists is what you get.
  • Share this list as names the list, putting it under lists so another template can bind to the same rows — a UI template and an API template over one set of sites.

Add another variable for a grid: sites × apps, every combination.

Saving expands the template first. An unknown variable, a name that does not vary, or a grid over the cap is refused with the reason — a template that cannot expand would take the whole config down with it, and every rule off the dashboard.

Linked, or just a head start

Saving a template asks which you want:

  • Keep them linked (default) — one definition, rules derived from it. Change it once and all of them follow. For targets that move together.
  • Create as separate rules — writes them out in full and drops the template. Each is an ordinary rule you can edit; nothing keeps them in step afterwards. For targets that only start the same.

You do not have to decide up front, because of the next part.

Detaching one rule

Detach this rule, on any generated rule, writes that one out in full as an ordinary editable rule. The template carries on producing the others.

Start linked; detach the one that turns out to need something the rest don't. There is no third list of exceptions to maintain: a rule written out in full simply wins over a template that would produce the same name, so the template stops producing it — and deleting the detached rule puts it back.

Looking at what a template produced

Opening a generated rule shows it resolved and read-only — real paths, real profile, exactly what that site will do — with the parts that came from its row highlighted, and a legend saying which token supplied them. Run and Dry run work as normal; Edit template is in the banner and the footer.

Nothing on it can be edited, and not only the fixed half: the config holds one entry, the template, and the generated rule is re-derived on every load. There is nowhere to put a change to this rule, so a partly-editable form would either do nothing or silently change every sibling. Anything that differs per row belongs in a column. For a genuine one-off, delete that row and Duplicate any generated rule — the copy is a real, standalone, fully editable rule.

On the rules screen, everything one template produced is gathered under a heading naming it, with an Edit template link — so twenty rules say "there is one template, and it is here" rather than saying "from a template" twenty times.

The rule that keeps "back up then delete" safe

Delete local files is rejected unless a back up or upload step comes before it — enforced in the editor, again before the file is written, and again when the config loads. Order matters: deleteLocal then backup is refused.

At run time it goes further. Each step records which files it verified landed on the server (size-checked, manifest-recorded). deleteLocal deletes only those files. A file whose upload failed is kept and logged:

kept src/ui/app.js — not confirmed on the server this run

So a half-failed transfer can never take your local copy with it.

Running

  • Dry Run Rule — every step, what it would touch, destructive ones flagged.
  • Run Rule — executes the pipeline, with live progress per file.
  • Run All Rules — every enabled rule in order.
  • Run selected — tick the rules you want on a profile card and press Run selected on that card. The tick beside Rules (n) takes the lot. Tick rules on a second profile and a page-wide bar appears to run all of them.

The same bar carries a menu of everything else a selection can do:

Dry run selected Every plan in one document, not a window per rule.
Enable / Disable selected No confirmation — it undoes itself.
Duplicate selected Confirms, and shows the names the copies will get.
Delete selected Confirms and lists them. Removes them from the config; no files anywhere are touched.

Each says what it had to leave out rather than quietly doing less — a rule a template generated (edit the template instead), one that had already gone, one already in the state you asked for.

A switched-off rule can still be ticked, so you can switch it back on from here; running is the only action that skips it. A rule already on the deployment queue cannot be ticked at all, because it is mid-flight.

Everything runs on one queue: one rule at a time, in the order the config lists them — not the order you ticked them, so a rule that builds still runs before the rule that uploads what it built. A failure holds the rest back rather than deploying on top of it.

A batch is approved once. The prompt names every rule, the server each one goes to, and marks the ones that delete files or run a shell command; approve it and the whole batch runs unattended. A rule run on its own still gets the full review screen, and a queued rule whose plan grew while it waited still stops and asks — an approval given ten minutes ago cannot cover a deletion that has since got bigger.

Disable a rule to keep it but skip it in Run All — a disabled rule cannot be ticked either, and the confirmation names anything it left out rather than dropping it quietly.

All the rules on a profile

A profile card shows its first two rules. View all opens the rest on a page of their own — three across, with its own search and the same select-and-run — rather than growing the card until it pushes every other profile off screen. Also on the palette as FTP Deploy: All Rules For This Profile.

Repeating a rule

Duplicate rule — the icon on every row — writes a copy named <rule>-copy and offers to open it. Use it for one-off variants.

For anything repetitive, use a rule template instead. Thirty-two sites with a UI and an API each is 128 copies to maintain, where every change has to be made 128 times and the one you miss is a live site; the same estate is one template that expands into 128 rules, and one edit changes all of them.

Database deployment

A developer changes something on their local database and now dev needs the same change. Instead of pasting the script into SSMS by hand, commit it — and let the rule that deploys the site apply it too, in the same approved run.

Set up the database on a profile

A profile answers "where do the files go". Open it and turn on Database to have it also answer "and which database is this environment":

{
  "name": "dev",
  "protocol": "local",
  "remoteRoot": "//WEB01/d$/Sites/app",

  "database": {
    "engine": "sqlserver",          // sqlserver | mysql | postgres
    "host": "DEVSQL01",             // an instance goes here too: DEVSQL01\\SQLEXPRESS
    "database": "AppDb",
    "username": "deploy_svc",       // omit entirely for Windows authentication
    "trustServerCertificate": true
  }
}

The password lives in the OS keychain, never in this file. A profile with no database block never connects to one and is never asked about SQL — its rules deploy files and nothing else.

Where the scripts live

One folder per release, numbered scripts inside it:

db/
  v1.21.0/
    1.sql
    2.sql
  v1.22.0/
    1.sql
    2.sql
    3.sql      ← added today; 1 and 2 already went to dev
  v1.22.1/
    1.sql

FTP Deploy: New Database Script creates the next correctly numbered file in the newest version folder and opens it, so the numbering never has to be worked out by hand.

Version folders run in version order and the files inside them in number order. That is a real rule, not an accident of sorting: as plain text v1.10.0 comes before v1.9.0 and 10.sql before 2.sql, which would apply a migration against a schema that never had the one before it. A folder that is not a version — v1.22.1 (copy) — is named and refused rather than sorted last, and a .sql sitting outside a version folder is refused too. There is no position for it that is safe to guess.

The rule

{
  "name": "Dev — database + site",
  "profile": "dev",
  "actions": [
    { "type": "dbBackup" },
    { "type": "sql", "from": "db" },
    { "type": "upload", "overwrite": "ifChanged" }
  ]
}

Order matters, which is why these are steps in the pipeline rather than something beside it. Schema changes usually have to land before the code that depends on them; a dropped column has to land after. Both are one drag in the editor. And if a script fails, the upload never runs — the site stays on the version that matches the schema it actually has.

What counts as a new script

A journal table in the target database, created on first run. Not file dates, and not the local manifest: the question is whether this database has had the script, and that has to stay true when a colleague or a build agent deploys between two of your runs.

Test connection never creates it. It reports whether the table is there and whether the account could create one, and if it is missing it explains what the table is for and offers a button. Otherwise the first run creates it, and the confirmation says so before you approve. A dry run never creates it at all.

  • Adding 3.sql to a folder whose 1.sql and 2.sql already ran is simply pending. That is what a release branch does all week.
  • A new script in a folder that sorts below the highest already applied — a v1.21.5 hotfix arriving after v1.22.0 shipped — is applied with a warning, or refused if you set "outOfOrder": "stop".
  • A script edited after it was applied stops the rule. The database ran one version of that change and the file now says another. Add a new script.
  • A renamed version folder is detected by content and held back, rather than re-running a release that already happened.

Most deploys change nothing, and stay quiet

With no pending scripts the step reports "nothing to apply", asks for no confirmation, and the file deploy carries on untouched. The database backup skips itself for the same reason — otherwise a rule run five times an hour writes five full backups of a database nothing touched.

FTP Deploy: Database Status answers "is there anything to run?" without arming a deploy: journal count, highest applied version, and the pending list in the order it would run.

Options on the step

Field Default What it does
from the rule's folder Folder holding the version folders.
layout versioned flat for a folder with no version subfolders.
select pending all re-runs everything — for rebuilding a scratch database.
outOfOrder warn Or stop.
onChangedScript stop Or warn, or rerun.
transaction perScript perVersion, all, or none.
onError stop Or continue.
timeoutSeconds 300 Per script.
upTo — Apply nothing above this version folder.
repeatable — Globs re-applied whenever their content changes — for CREATE OR ALTER procs.
variables — $(Name) tokens. An unresolved one is an error, never an empty string.
verify none parse compiles every script before running any of them.

One thing to know about database backups

Every file backup in this extension is downloaded to your machine, because a copy living on the machine it protects is not a backup. A database cannot honour that: SQL Server's BACKUP DATABASE writes to a path the database server's service account can see, and there is no other mode. So a dbBackup step on SQL Server is a rollback point, not an off-box backup, and the editor, the dry run and the confirmation all say so. MySQL and PostgreSQL use mysqldump and pg_dump, which do write locally — and do need the tool on PATH.

There are no down-scripts. If a migration succeeds but is wrong, the backup is the only way back.

Deploying without a server: the local protocol

When the target is a folder this machine can already see, there is no reason to put a server in front of it. Pick Local folder or network share as the protocol and give the profile a path instead of a host:

{
  "name": "iis",
  "protocol": "local",
  "remoteRoot": "C:/VirtGateWeb"
}

No host, no port, no credentials, nothing in the keychain. Everything else is unchanged — hash-based incremental transfer, dry runs, backups, per-file progress, the manifest, and every kind of rule step.

This covers three cases:

  • IIS on this machine. A site's physical path is a folder; deploying to it is a copy. Note that an application's physical path is often outside the site root, so an FTP site rooted at wwwroot cannot even reach it without a virtual directory per application.
  • A UNC path — //WEB01/d$/Sites — or a mapped drive, which is how most on-prem servers are reachable from a build machine on the same domain.
  • A staging folder that something else picks up.

remoteRoot takes a drive path (C:/Sites/app), a UNC path (//WEB01/d$/Sites) or a plain POSIX path. Write it with forward slashes; backslashes are accepted and converted. The folder must exist and be writable — that is checked when the profile connects, rather than partway through a deploy.

Uploads are still atomic: the file is written as <name>.part and renamed into place, which on a local disk is a genuine atomic rename rather than the best-effort version FTP gives you.

Combined with a rule template, one profile covers an estate:

{
  "lists": { "sites": ["BoostOnlineWork", "…"] },
  "rules": [
    {
      "name": "${site} — UI",
      "forEach": { "site": "sites" },
      "profile": "iis",
      "localPath": "builds/${site}/ui",
      "remotePath": "/${site}/VirtGatePortal",
      "actions": [{ "type": "upload", "overwrite": "ifChanged" }]
    },
    {
      "name": "${site} — API",
      "forEach": { "site": "sites" },
      "profile": "iis",
      "localPath": "builds/${site}/api",
      "remotePath": "/${site}/VirtGatePortalAPI/bin",
      "actions": [
        { "type": "backup", "source": "remote" },
        { "type": "upload", "overwrite": "ifChanged" }
      ]
    }
  ]
}

A note on the API path: deploying into bin replaces assemblies the app pool may hold open. Classic ASP.NET shadow-copies its bin and usually tolerates this; ASP.NET Core does not, and needs either its shadow-copy setting enabled or an app_offline.htm pair of steps — see Cleanup steps that always run.

Where profiles and rules are stored

By default they live in the folder that uses them, at .vscode/ftp-deploy.json — committed with the project, reviewed with the project.

If you deploy the same handful of servers from a dozen different folders, keep one set for the whole machine instead. Run FTP Deploy: Config Location (This Folder or This Machine) and pick This machine:

This folder This machine
Profiles and rules .vscode/ftp-deploy.json ~/.ftp-deploy/ftp-deploy.json
Upload state .vscode/.ftp-deploy-manifest.json ~/.ftp-deploy/manifest.json
Passwords keychain, per folder keychain, once per machine
Active profile, run history per folder shared

Switching offers to copy the current folder's config across, and leaves the other file untouched — switching back loses nothing. Passwords are namespaced per scope, so set them once after a first switch.

Rules stored for the machine should use absolute local paths: there is no one folder for a relative path to resolve against, and they may run in a window with no folder open at all.

How change detection works

Deploy compares local files against the manifest, not against the server.

FTP exposes no file hash, and its timestamps drift with server timezone and DST, so remote metadata cannot reliably answer "did this change?". Instead, every confirmed upload is recorded as sha256 + size + timestamp. The next run hashes locally and uploads only what differs.

Commit .vscode/.ftp-deploy-manifest.json to git so every machine deploys the same incremental set. Delete a profile's section — or run Reset Manifest — to force a full re-upload.

Backups

A Back up step copies files into a timestamped folder on your disk — 2026-08-17_143052/ — before anything overwrites or deletes them. It can copy either side: the server files about to be replaced, or your local files.

  • The folder it writes into comes from the step, or from localBackupRoot when the step leaves it blank.
  • snapshotFolderFormat names the subfolder. Tokens: YYYY MM DD HH mm ss RULE.
  • snapshotWriteReceipt adds a MANIFEST.txt listing every file's hash and size, so a backup can be verified or restored without this extension.

Every step gets a folder of its own. The name is claimed, not assumed: if it is already taken, the next one gets a -2 suffix. This matters because a queue runs rules back to back, so several can resolve to the same clock name — and two backups sharing a folder do not fail, they interleave, leaving something that looks complete and is a mixture.

When several rules share one backup root, put RULE in the format — RULE_YYYY-MM-DD_HHmmss — so each folder says which rule wrote it. Twenty-five folders named only by the clock say nothing about which site each came from.

Backups are always complete, never incremental, and are never written to the server. Nothing prunes them — delete old folders yourself.

Safety

  • Dry run first. confirmBeforeDeploy (default on) shows the plan, then asks for a modal confirmation.
  • Atomic writes. Every file uploads to <name>.part and is renamed into place. A dropped connection leaves an orphaned .part, never a truncated web.config serving 500s.
  • A dropped connection is reopened, not retried against. The connection is released before a command or database step — hosts close a session that goes quiet for a couple of minutes, and a build is quiet for longer — and reopened by the next step that needs it. A session lost mid-transfer is reconnected on the retry.
  • Size verification after each transfer, before the manifest records it.
  • Manifest written per file, not at the end — an interrupted run leaves a manifest describing exactly what actually landed, and the next run resumes.
  • Deletes are opt-in. mirrorDeletes is false by default. Files gone locally are reported as orphans and left alone.
  • An approval covers what it was given for. A queued rule's plan is rebuilt just before it runs, because the rules ahead of it may have changed what it matches. If it now deletes more than it did when you approved it, the queue stops and asks again rather than treating the old yes as covering the new number.
  • Excludes always beat includes, plus a built-in default exclude list (node_modules, .git, dist, .env, *.log…).
  • Secrets live in the OS keychain, never in the config file.

Settings

FTP Deploy: Open Settings, or search ftpDeploy in the Settings UI. Per-server details live in profiles; these are the behaviour knobs.

Setting Default What it does
configScope workspace workspace = this folder, global = ~/.ftp-deploy/ for the whole machine.
configPath .vscode/ftp-deploy.json Where profiles live, in workspace scope.
manifestPath .vscode/.ftp-deploy-manifest.json Where upload state lives.
showStatusBar true Active profile in the status bar.
confirmBeforeDeploy true Modal confirmation before transferring.
openPlanInEditor false Also open the plan as a text document.
verifySizeAfterUpload true Check remote byte count before recording.
confirmDeletes true Separate second prompt for remote deletions.
retries 3 Per-file attempts; backoff 2s/4s/8s.
commandTimeoutMs 600000 How long a command step may run.
listConcurrency 4 Connections the rule editor lists server folders over. 1 if the host refuses extra sessions.
uploadConcurrency 4 Connections a deploy uploads over. Each file is several round trips whatever its size, so a few sessions taking turns cut the wall clock roughly in proportion. 1 if the host limits logins per account.
connectTimeoutMs 20000 Connection timeout.
maxFileSizeMb 0 Skip files over this size. 0 = no limit.
useDefaultExcludes true Apply the global exclude list.
defaultExcludes (11 globs) Excluded from every profile. Fully editable.
followSymlinks false Follow links when scanning.
localBackupRoot (empty) Default folder for back up steps that name none.
snapshotFolderFormat YYYY-MM-DD_HHmmss Name of each backup subfolder. Tokens YYYY MM DD HH mm ss RULE.
snapshotWriteReceipt true Write MANIFEST.txt into each backup.

Config reference

For hand-editing .vscode/ftp-deploy.json. It accepts // and /* */ comments.

Key Required Notes
name ✅ Unique. Keys the credential and manifest section.
protocol ✅ sftp | ftp | ftps | local
host / username ✅ Not used by local, which addresses a folder.
remoteRoot ✅ Absolute remote path, or a folder path when protocol is local.
port Defaults to 22 (sftp) or 21. Ignored by local.
privateKeyPath SFTP only. Passphrase goes in the keychain.
localRoot Workspace-relative. Defaults to the workspace root.
include / exclude Globs. Include builds the set, exclude subtracts.
environment development | staging | production. Colours the badge.
mirrorDeletes Default false.
secureRejectUnauthorized ftps only. Leave true unless you know why.
database The database for this environment. Omit for a files-only profile.

database

Key Required Notes
engine ✅ sqlserver | mysql | postgres
host ✅ May carry a SQL Server instance: DEVSQL01\SQLEXPRESS.
database ✅ The database scripts are applied to.
username Omit for integrated auth. Nothing is prompted or stored.
port Defaults to 1433 / 3306 / 5432.
encrypt Default true.
trustServerCertificate For a self-signed certificate on an internal server.
journalTable Default dbo.__FtpDeployJournal. Created on first run.
connectTimeoutSeconds Default 15.

Known limits

  • Concurrency is per session, not per file. Both FTP and SFTP clients drive a single channel, so uploadConcurrency opens that many sessions and each carries one file at a time. A host that caps logins per account caps the speed-up too; the log says when an extra session was refused.
  • Depth is not parallelisable. A folder must be listed before its children are known, so a deep chain of single folders is one round trip at a time however many connections are open. Wide trees — a resources folder per locale — are where a pool pays.
  • No scheduling. An extension only runs while VS Code is open; there is no background activation.
  • No restore/download yet.
  • No down-scripts. A migration that succeeds but is wrong can only be undone from the backup. That is why dbBackup is worth keeping in the rule even though the schema marks it optional.
  • The database has to be reachable from this machine. Normally true of dev and usually false of production. A firewalled production database is a good reason for the rule that targets it to live on a build agent rather than a laptop — and a laptop is a poor place to run a forty-minute migration anyway.
  • No schema comparison. The scripts are written by you and reviewed in the pull request. If you want a generated diff, a command step running SqlPackage.exe /Action:Script can write one into a version folder for the sql step to apply — which keeps it reviewable on disk rather than executing straight into a database.

Building from source

npm install
npm run package     # runs the tests, then builds ftp-deploy-<version>.vsix
code --install-extension ftp-deploy-1.0.0.vsix --force

Reload the window afterwards. F5 launches an Extension Development Host instead, and npm run watch rebuilds on save.

src/
  config/     types, settings, JSONC read + write, SecretStorage
  core/       scanner (globs), hash, manifest, plan, rule engine
  transport/  Transport interface, sftp + ftp impls, atomicPut, retry
  commands/   one file per user-facing action
  ui/         dashboard, profile and rule editors, deployment panel

core/ and transport/ are free of vscode imports on purpose — that is what keeps a future CLI possible.

License

MIT

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft