How the sausage gets made

  • The image is Debian trixie with Wine 11 from WineHQ, pinned to one version (the 64-bit half plus the 32-bit half that MT4’s ancient MetaEditor needs; Debian’s own Wine 10 can’t run MetaTrader 5 build 6304 and later, which bails out with “A debugger has been found running in your system”), Xvfb as a fake display nobody ever looks at, and one Bash script, entrypoint.sh. Not a single MetaQuotes byte in it.
  • The toolchain: on the first run the script downloads the MT4 and MT5 installers, checks each one’s Authenticode signature (valid digest, a certificate chain the image’s CA bundle trusts, and a signing certificate whose common name starts with MetaQuotes; anything else gets refused), and installs both silently under Wine. If the certificate authority’s revocation server is down, it retries three times, then checks everything but revocation and logs a warning with reason=crl_unreachable. The installers default to RoboForex’s; point MQL_COMPILER_MT4_URL and MQL_COMPILER_MT5_URL at any broker’s or MetaQuotes’ own. The compiler inside is the same either way.
  • The standard library: the installers don’t actually put it on disk. A terminal unpacks its MQL4/MQL5 directory, standard headers included, the first time it starts. So setup starts each terminal once, headless, waits for Trade/Trade.mqh (MT5) and stdlib.mqh (MT4) to show up, and kills it. A few seconds each, once.
  • The cache: Wine itself is in the image; the Wine prefix with MT4 and MT5 in it is about 2 GB. Once a terminal is installed and has unpacked its standard library, it gets moved out of Program Files into a plain portable directory (drive_c/terminals/mt4, drive_c/terminals/mt5), so the cached terminals are the exact same kind of thing as one you bring yourself. A cache from an older version gets that move on its next run, no re-download. The prefix lives in the Docker volume mql-compiler-cache, one directory per user ID, so everybody on a box shares a single volume. docker volume rm mql-compiler-cache nukes it and the next run installs again. The container runs as your UID, so the compiled files are yours.
  • The mounts: the command turns every path into an absolute one and mounts the directory whose Include it needs at the same path inside the container: the nearest MQL4 above a .mq4, the nearest MQL5 above a .mq5, the nearest of either above a directory, or the directory itself or the file’s directory when there’s no such parent. Nested ones collapse into one mount. A --mt4-terminal/--mt5-terminal directory is mounted read-only on top. It won’t mount /, so a bare mql-compiler from / is refused. The container starts in your current directory, so the paths in the output are your real paths. On SELinux hosts, the mounts carry no :z label, so expect permission errors there.
  • The seccomp profile: Wine 11 calls userfaultfd while it starts. Docker’s default seccomp filter doesn’t allow that call, and under Linux 7.0 (Ubuntu 26.04, GitHub’s ubuntu-latest) Wine then dies with wine: socket : Function not implemented before the prefix even exists. seccomp/wine.json is Docker’s own default profile (moby/profiles seccomp/v0.3.0, checksum pinned, rebuilt with make seccomp from the Makefile) with that one call allowed; the kernel’s vm.unprivileged_userfaultfd rule still applies. The installed command, the Action and the tests all pass it to docker run.
  • Compiling: every file goes through MetaEditor’s command-line compiler with /inc pointed at the merged directory from how includes get resolved. MetaEditor’s exit code is a lying piece of shit (it’s the number of files that compiled, so 1 when everything went fine and 0 when it failed), so the script ignores it, reads the Result: line of the compile log instead, and turns the Windows paths in there back into yours.