
I stopped hand-carrying EA updates to my VPS — a git-based auto-deploy for MT5
How I built a push-to-deploy pipeline for a live MT5 expert advisor with two PowerShell scripts and Task Scheduler — including four traps I hit, like MT5 not reloading a replaced .ex5.
The expert advisors on this site run 24/5 on MT5 inside a Windows VPS, while all development happens on my local machine. Every EA change used to require this ritual:
- Compile and test locally, push to git
- Log into the VPS over Remote Desktop (RDP)
git pullon the VPS- Recompile in MetaEditor
- Manually restart the EA on its chart
A few minutes each time, but with real money attached, every step demands attention. And in my setup, most EA code changes are made by an AI agent (Claude Code) — which meant the human’s only remaining job was being a courier who carries the AI’s work to the VPS. Time to automate the courier away.
The finished shape
The end result: one command on the dev machine, everything else happens on its own.
Dev PC: run deploy_ea.ps1
├ compile every EA via MetaEditor CLI (abort on any error)
└ commit & push both .mq5 and .ex5
↓ (GitHub)
VPS: Task Scheduler runs pull_deploy_ea.ps1 every 5 minutes
├ git pull if there are new commits
├ compare repo .ex5 vs deployed .ex5 by SHA256
├ copy only the changed ones into MQL5\Experts (old ones archived)
└ restart the terminal that received a new binary
↓
MT5: restores charts, EAs, inputs and AutoTrading on startup
└ the EA sends a Discord webhook on init ← deployment confirmed
I went with a pull-based design: the VPS polls GitHub on a schedule. The alternative — pushing binaries into the VPS over SSH — would mean running an SSH server on a Windows box and opening an inbound port, which is more surface to manage. Pull needs no inbound path at all, and a five-minute delay is irrelevant for EA updates.
Compilation happens only on the dev machine; the VPS receives the finished .ex5. A compiled .ex5 is platform-independent bytecode tied to neither broker nor PC, so where you compile makes no difference. Committing binaries to git felt wrong for a moment, but they are a few hundred KB, and being able to trace exactly which commit’s binary is live in production turned out to be a feature.
Four traps I fell into
It did not work on the first try. The traps are probably the useful part of this article.
Trap 1: the same source produces a different .ex5 every time
To implement “deploy only when the binary changed,” I compiled identical source twice and compared SHA256 hashes. They differed every time — the MQL5 compiler embeds compile-time metadata into the binary.
The fix lives on the sending side: if a given EA’s .mq5 has no git diff, its recompiled .ex5 is reset to the committed version and never ships. Running the deploy command twice by accident now results in a clean no-op instead of a pointless EA restart on a live account.
Trap 2: the PowerShell script died without leaving a log line
The VPS log stopped like this:
00:27:05 new commit detected: afb3a72 -> 6e7664a
(nothing after this)
The script always writes either a pull error or “pull done” — neither appeared. The culprit is a classic Windows PowerShell 5.1 behavior: with $ErrorActionPreference = "Stop", running git pull 2>&1 turns git’s normal stderr progress output (From github.com...) into a terminating exception. The script died mid-flight, and without a catch block, silently.
Three fixes: treat git’s exit code as the only success signal instead of letting stderr kill the script, wrap the whole thing in try/catch so any unexpected exception is logged as [FATAL], and — most importantly — change the deploy decision from “did the pull change anything” to a direct hash comparison between the repo’s .ex5 and the deployed .ex5 on every run. That last one makes the pipeline self-healing: whatever fails mid-way gets repaired automatically five minutes later.
Trap 3: MT5 does not reload a replaced .ex5
The biggest surprise. MT5 famously reinitializes a running EA when you recompile it (REASON_RECOMPILE), so I assumed overwriting the .ex5 file would trigger the same thing.
Tested on the live terminal: the file updated, the running EA kept executing the old code. The auto-reinit only fires on recompilation from MetaEditor; an external file copy is invisible to the terminal, which loaded the program into memory at attach time and never looks back at the disk.
The pragmatic fix: the deploy script restarts the whole terminal when (and only when) it delivered a new binary. Sounds crude, but MT5 restores charts, EAs, input parameters and the AutoTrading switch on startup — semantically identical to the manual “restart the EA” step this pipeline replaced. Positions live on the server anyway. The restart asks the process to close gracefully (WM_CLOSE), waits up to 60 seconds, force-kills only as a last resort, then relaunches.
Trap 4: Task Scheduler can’t close a window it can’t see
Trap 3 has a sequel. A task registered the ordinary way runs in a non-interactive session, from which the MT5 window in the RDP session has no visible window handle — the graceful-close request goes nowhere and every restart becomes a force-kill.
Registering the task with the /IT (interactive) flag makes it run inside the logged-on user’s session, where closing the window works properly. The “only runs while logged on” constraint is irrelevant here, because MT5 itself only runs in that logged-on session. A disconnected RDP session (disconnect, not sign out) still counts as logged on — verified end-to-end with RDP disconnected: deploy, terminal restart, Discord notification, all hands-free.
Safety rails
A pipeline that ships binaries into a live account unattended deserves generous guardrails:
- A single compile error aborts the whole deployment — a broken binary physically cannot reach the VPS
- Every overwritten
.ex5is archived with a timestamp first (instant manual rollback) - The proper rollback is just
git revert+ push — the old binary rides the same pipeline back out - The VPS authenticates with a read-only GitHub deploy key: a compromised VPS cannot tamper with the repository
- The EA sends a Discord webhook on startup, so “notification arrived on my phone” doubles as free deployment confirmation
Making the AI agent incapable of forgetting
Since an AI agent writes most EA changes, the new failure mode becomes “AI edits the code, forgets to deploy.” I closed that with the agent harness itself: a Claude Code hook fires the moment any .mq5 file is edited and injects a reminder into the agent’s context — finish this task by running the deploy script, no manual compiles, no partial commits. The same rule is written into the project’s instruction file, so it is enforced twice. In the final rehearsal the AI edited an EA, the hook fired, the AI ran the deployment itself, and the Discord startup notification arrived — zero human actions.
The result
- Shipping an EA update is now: one command, wait five minutes, watch Discord
- RDP logins disappeared from daily operations
- The forget-to-compile and forget-to-restart failure modes disappeared with them
The whole thing is just two PowerShell scripts and Task Scheduler — nothing exotic. But the details (MT5’s reload behavior, PowerShell 5.1’s stderr trap, session isolation in Task Scheduler) only surface when you run it against a live terminal, so hopefully this saves someone the same detours.