Loïc Faugeron Technical Blog

Turn your PHP app into a standalone binary 25/03/2026

TL;DR:

  1. Prepare app for prod environment: no dev deps, autoloading optimisation (classmap authoritative), making .env.local.php from .env, no debug symfony cache, etc
  2. Compile the PHAR with Box
  3. Concatenate micro.sfx + dtk.phar into a self-contained binary with static-php-cli

I've been building DTK, a PHP CLI tool that automates the repetitive ceremony around the developer workflow: open ticket, create branch, open PR, merge, deploy - all wired together so you don't have to context-switch between your terminal, your Kanban board, and GitHub.

It's a Symfony Console app, it runs fine with php dtk, but distributing it to teammates means they need PHP installed at the right version, with the right extensions, plus Composer. That's friction I'd rather not ask anyone to deal with.

Turns out, PHP can produce a single self-contained binary, no PHP required on the target machine. I learned this from a talk by Jean-François Lépine at Forum PHP 2025, PHP without PHP: Make Standalone Binaries from Your Code, which is in French, but here's an English recap of it.

Surprisingly easy to set up. Here's how.

The two ingredients

Two tools do all the work.

Box packages a PHP project into a .phar archive. A .phar is a self-contained PHP archive: it includes all your source files and vendor dependencies, and PHP can execute it directly.

PHP Micro SFX, part of the static-php-cli (SPC) project, is a minimal static PHP binary with no external dependencies. It reads whatever binary data is appended to it and executes it as a .phar.

Combine the two:

micro.sfx + app.phar = standalone binary

One file. No PHP needed on the target machine. Drop it, run it.

Step 1: package the app as a PHAR

Box reads a box.json config file and produces the archive. Here's the one for DTK:

{
    "$schema": "https://box-project.github.io/box/schema.json",
    "main": "dtk",
    "output": "build/dtk.phar",
    "compression": "GZ",
    "check-requirements": false,
    "directories": [
        "config",
        "src",
        "var/cache/prod",
        "vendor"
    ],
    "files": [
        ".env.local.php"
    ]
}

A few things worth noting:

Step 2: get the micro binaries

static-php-cli prebuilds micro SFX files for all major platforms and PHP versions, so you don't need to compile anything yourself.

In the DTK Dockerfile, I download them all at image build time:

RUN for PLATFORM in linux-x86_64 linux-aarch64 macos-x86_64 macos-aarch64; do \
        curl -fsSL \
            -o /tmp/micro.tar.gz \
            "https://dl.static-php.dev/static-php-cli/common/php-${PHP_VERSION}-micro-${PLATFORM}.tar.gz" \
        && tar xzf /tmp/micro.tar.gz -C /usr/local/lib/ \
        && mv /usr/local/lib/micro.sfx "/usr/local/lib/micro-${PLATFORM}.sfx" \
        && rm /tmp/micro.tar.gz; \
    done

Windows has a separate download (a zip, not a tarball), but same idea.

The micro SFX from static-php-cli includes a whole bunch of extensions, so if you need some that are missing, or if you want the bare minimum, you'd need to compile your own micro using SPC, that's more involved, but SPC has a doctor --auto-fix command to help with the build environment setup.

Step 3: assemble the binaries

With the PHAR built and the micro SFX files in place, combining them is a cat 😼:

cat micro-linux-x86_64.sfx dtk.phar > dtk-linux-x86_64
chmod +x dtk-linux-x86_64

That's it. The resulting file is a valid ELF binary (or Mach-O on macOS, PE on Windows) that carries its own PHP interpreter alongside the application code.

Here's the full build script I use for DTK (bin/mk-dtk-bin.sh) that does all of it:

#!/usr/bin/env bash
set -euo pipefail

# Restore dev dependencies once finished
trap 'composer install --optimize-autoloader --quiet' EXIT

echo '  // Installing prod dependencies...'
composer install --no-dev --classmap-authoritative --quiet

echo '  // Compiling environment variables...'
php bin/mk-dtk-bin/dump-env-prod.php

echo '  // Warming up Symfony cache...'
APP_ENV=prod APP_DEBUG=0 php bin/console cache:warmup --quiet

echo '  // Building PHAR...'
mkdir -p build
php -d phar.readonly=0 /usr/local/bin/box compile

echo '  // Assembling binaries...'
for _PLATFORM in linux-x86_64 linux-aarch64 macos-x86_64 macos-aarch64 windows-x86_64; do
    case "${_PLATFORM}" in
        windows-*) _EXT='.exe' ; _CHMOD=false ;;
        *)         _EXT=''     ; _CHMOD=true  ;;
    esac

    cat "/usr/local/lib/micro-${_PLATFORM}.sfx" build/dtk.phar > "build/dtk-${_PLATFORM}${_EXT}"
    ${_CHMOD} && chmod +x "build/dtk-${_PLATFORM}${_EXT}"
done

echo '  // Generating checksums...'
sha256sum \
    build/dtk-linux-x86_64 \
    build/dtk-linux-aarch64 \
    build/dtk-macos-x86_64 \
    build/dtk-macos-aarch64 \
    build/dtk-windows-x86_64.exe \
    > build/checksums.txt

echo '  [OK] Binaries built'

A few things the script does before building the PHAR:

The trap at the top restores dev dependencies when the script exits, so the local dev environment is left intact after a build.

What comes out

Running make app-bin in the Docker container produces:

build/dtk.phar
build/dtk-linux-x86_64
build/dtk-linux-aarch64
build/dtk-macos-x86_64
build/dtk-macos-aarch64
build/dtk-windows-x86_64.exe
build/checksums.txt

Five binaries, one per platform, from a single command, without leaving Docker. Each one runs without PHP on the target machine.

Constraints worth knowing

This is real PHP, the same interpreter, the same extensions, the same behaviour. A few things to be aware of:

FFI is not available. Foreign Function Interface calls (PHP calling C libraries directly) don't work in static builds. For a CLI tool this is unlikely to matter.

Binary size. A minimal PHP binary with no extensions is around 3 MB. DTK, which only uses standard extensions, comes out much smaller than a full PHP install. Not Go-binary small, but perfectly acceptable for a CLI tool distributed via GitHub Releases.

Startup time. There's a small overhead compared to running php dtk directly: PHAR extraction adds a few milliseconds, and the static build uses musl libc rather than glibc, which is slightly slower. For a developer tool where the user is waiting hundreds of milliseconds anyway, this doesn't matter.

Not for web apps. This is for CLI / TUI / scripts, for web PHP apps, use FrankenPHP instead. It's a production-proven PHP app server built on top of static-php-cli that handles all the complexity, and it ships as a standalone binary too.

A surprisingly short path

The whole thing (Box config, Dockerfile setup, build script) took an afternoon. Most of that time was reading the static-php-cli docs and figuring out the Docker layering. The actual concatenation step (cat micro.sfx app.phar > binary) was the part that surprised me most: something that powerful should not be that simple :D !

If you're building a PHP CLI tool meant to be distributed to people who shouldn't need to care about PHP, this is the approach. It works, it's well-supported (FrankenPHP, Laravel Herd, and NativePHP all use static-php-cli under the hood), and the tooling is solid.

If you want to see a real-world example of a PHP project that compiles its own micro binaries (including the SPC setup), look at Castor. Castor is a task runner / script launcher for PHP (think Make or Taskfile, but in PHP) and it ships prebuilt binaries for all platforms. Its build setup is a good reference for when you outgrow the prebuilt micro SFX files and need to compile your own with a custom extension set.

The full DTK source used in this article is available at github.com/gnugat/dtk/tree/v0.1.0.