DEV Community

Cover image for WinRavel: I Bundled an Entire Laravel App into a Single Windows .exe (FrankenPHP + MariaDB inside)
Taqin
Taqin

Posted on

WinRavel: I Bundled an Entire Laravel App into a Single Windows .exe (FrankenPHP + MariaDB inside)

WinRavel: Bundle a Laravel App into a Single Windows .exe

Laravel is great — until you have to ship it to someone who doesn't have PHP, Composer, a web server, or any idea what a "database" is. So I built WinRavel: point it at a Laravel project, click Build, get one .exe that just runs.

No PHP install. No XAMPP. No composer install. Double-click → a dark control window opens → your app serves on 127.0.0.1.

What's inside the exe


  • FrankenPHP (PHP 8.5 + Caddy) — the app server, baked in
  • Your Laravel app — fully set up at build time (composer install, key:generate, migrate, optional db:seed)
  • SQLite (default) or a bundled MariaDB server for apps that need real MySQL
  • Optional source obfuscation of app/

On first run it extracts to %APPDATA%\<App>, then frankenphp php-server serves it. A Windows Job Object ties the PHP (and MariaDB) process to the launcher so nothing orphans.

The interesting engineering bits

1. No recompile per build (self-extracting).
The launcher is a precompiled C# stub with FrankenPHP's runtime embedded. Building an app just appends a payload — [app.zip][config][footer] — to a copy of that stub. The stub reads the trailing bytes of its own exe at startup. Result: builds are instant, and the build machine doesn't need the .NET SDK.

2. Bundling MariaDB without 250MB.
MySQL/Postgres have no real embedded mode for PHP (and PGlite is WASM/JS — useless from PHP's pdo). So for "real MySQL" I bundle a trimmed MariaDB: just mariadbd + a few DLLs + a pre-initialized template data dir. At build time WinRavel boots it, creates the DB, runs migrations, shuts it down, and ships the migrated data dir. The launcher starts/stops it automatically. Shrinking InnoDB's redo log (96MB → 8MB) keeps it lean.

3. Obfuscating PHP that still runs.
Each app/*.php file becomes <?php return eval(gzinflate(xor(base64_decode('...'))));. Namespaces and class names stay intact (they live inside the eval'd code), so Laravel's reflection/autoload still works. Honest caveat: pure-PHP obfuscation is deterrence, not DRM.

4. SQLite ≠ MySQL.
A "Force SQLite" toggle rewrites a MySQL .env to SQLite — handy, but it'll break on MySQL-specific SQL. That's why the MariaDB bundle exists.

Stack

C# / .NET 10 (WinForms builder + launcher) · FrankenPHP · Caddy · MariaDB · Inno Setup.

Try it

👉 github.com/fdciabdul/WinRavel — download WinRavel-Setup.exe from Releases.

Top comments (0)