DEV Community

David
David

Posted on

Symfony + FrankenPHP: A Modern Stack for Developer Tools

PHP is boring. That's exactly why we chose it.

When we set out to build 19 developer utility websites, we needed a stack that was fast to develop with, easy to deploy, and wouldn't give us surprises at 3 AM. Symfony + FrankenPHP turned out to be the perfect combination.

Why Symfony in 2026?

Symfony isn't the cool kid. It doesn't have a hype cycle. Nobody's writing "I rebuilt my app in Symfony" Twitter threads. And that's the point.

What Symfony gives us:

  • Mature routing, templating (Twig), and dependency injection
  • First-class HTTP foundation
  • A component library that's been battle-tested for 20 years
  • Incredible backwards compatibility — upgrades don't break things
  • A generator ecosystem (MakerBundle) that scaffolds fast

For developer utility tools — where the backend is mostly "render a page and serve some static logic" — Symfony is wildly overqualified. And that's fine. Overqualified means it handles every edge case without you thinking about it.

Enter FrankenPHP

FrankenPHP changed the game for PHP deployment. If you're still running nginx + php-fpm, you're doing too much work.

What Is FrankenPHP?

FrankenPHP is a modern PHP application server built on top of Caddy. It:

  • Serves PHP applications directly (no separate php-fpm process)
  • Handles HTTPS automatically (Caddy's automatic certificate management)
  • Supports HTTP/2 and HTTP/3 out of the box
  • Runs as a single binary
  • Has a worker mode that keeps your application in memory between requests

Worker Mode Is the Secret Weapon

Traditional PHP: every request boots the framework, processes the request, tears everything down. It's the PHP lifecycle we've accepted for decades.

FrankenPHP worker mode: boots your Symfony app once, keeps it in memory, and handles subsequent requests without the bootstrap overhead.

# Our Dockerfile. That's it. Really.
FROM dunglas/frankenphp

COPY . /app

ENTRYPOINT ["frankenphp", "run", "--config", "/app/Caddyfile"]
Enter fullscreen mode Exit fullscreen mode

The result? Response times dropped from ~80ms to ~15ms. For simple utility tools, most of that 80ms was framework boot time. Worker mode eliminates it.

The Caddyfile

{
  frankenphp
}

localhost {
  root * /app/public
  encode zstd br gzip
  php_server
}
Enter fullscreen mode Exit fullscreen mode

That's the entire server configuration. No nginx.conf. No php-fpm pool tuning. No fastcgi_pass directives. Just... this.

Our Architecture

Here's how a request flows through our stack:

User → Cloudflare CDN → FrankenPHP (Caddy) → Symfony Router → Controller → Twig Template → HTML Response
Enter fullscreen mode Exit fullscreen mode

For most pages, Cloudflare serves a cached version and FrankenPHP never even sees the request. When it does, the response is generated in ~15ms.

Multi-Site Setup

We run 19 sites on a single FrankenPHP instance. The Caddyfile handles routing by domain:

namso.io {
  root * /app/sites/namso/public
  php_server
}

randomimei.com {
  root * /app/sites/randomimei/public
  php_server
}

randomiban.co {
  root * /app/sites/randomiban/public
  php_server
}

base64decode.co {
  root * /app/sites/base64decode/public
  php_server
}
Enter fullscreen mode Exit fullscreen mode

Each site is an independent Symfony application sharing common bundles. One process serves them all.

Deployment

Our deployment is embarrassingly simple:

  1. Push to GitHub
  2. GitHub Actions runs tests
  3. Build Docker image with FrankenPHP
  4. Deploy to VPS via SSH
  5. Purge Cloudflare cache

Total time: ~45 seconds. Zero downtime — FrankenPHP handles graceful restarts.

Performance Numbers

Metric nginx + php-fpm FrankenPHP worker mode
Average response time ~80ms ~15ms
Memory per worker ~30MB ~45MB (but shared)
Config complexity 3 files 1 Caddyfile
HTTPS setup Manual certbot Automatic
HTTP/3 support Extra config Built-in

The trade-off is slightly higher memory per worker, but since we're running a single instance serving all 19 sites, the total memory footprint is actually lower than running 19 separate php-fpm pools.

Why Not [Insert JS Framework]?

We get this question a lot. Why not Next.js, Nuxt, Remix, Astro...?

For our use case:

  • These tools don't need client-side routing
  • They don't need real-time updates
  • They don't need complex state management
  • They need to be fast and simple

Server-rendered HTML with vanilla JavaScript is the simplest architecture that could possibly work. And simplest usually means most reliable.

Lessons Learned

  1. FrankenPHP worker mode is production-ready. We've been running it for months with zero issues.
  2. Symfony's HTTP Cache component pairs beautifully with Cloudflare. Double-layer caching means our VPS barely breaks a sweat.
  3. Single binary deployment (FrankenPHP) eliminates an entire class of "works on my machine" problems.
  4. PHP is genuinely fast when you remove the per-request bootstrap overhead.

Try Our Tools

Built with this exact stack:

All free. Sub-second load times. Powered by Symfony + FrankenPHP.


Running FrankenPHP in production? I'd love to compare notes. What's your experience been like?

Top comments (0)