DEV Community

kristiansnts
kristiansnts

Posted on

Deploy Laravel on Vercel for Free — Even Heavy Filament Apps

How I bypassed Vercel's 250MB limit using a Go serverless proxy, compressed vendor, and a bundled PHP-FPM binary

The Problem

I wanted to deploy a Laravel + Filament app on Vercel's free Hobby plan. Two walls hit me immediately:

  1. 250MB size limit — Vercel serverless functions have a hard 250MB unzipped cap. My vendor/ directory alone was over 200MB with Filament, Dompdf, and other packages.
  2. HTTP 494 errors — Using SESSION_DRIVER=cookie (the only stateless option) caused "Request Header Too Large" because Filament stuffs too much data into the session cookie, exceeding Vercel's ~8KB header limit.

vercel-php is a great project, but it couldn't solve the size problem for heavy apps.

The Solution: A Go Proxy + Compressed Vendor

I built vercel-laravel-go — a Go serverless function that wraps PHP-FPM and compresses vendor from ~200MB down to ~30MB.

Here's what happens at a high level:

HTTP Request → Vercel → Go Handler → FastCGI → PHP-FPM → Laravel
Enter fullscreen mode Exit fullscreen mode

The Go function handles the entire PHP lifecycle:

  1. Bundles a static PHP-FPM binary at build time (no runtime download needed)
  2. Extracts vendor.tar.gz to /tmp/vendor on cold start (~30MB compressed vs 200MB+ raw)
  3. Creates symlinks so Composer's autoloader resolves classes correctly
  4. Redirects storage to writable /tmp/storage (since /var/task is read-only on Vercel)
  5. Starts PHP-FPM on a Unix socket and proxies every request via FastCGI

Why Go?

Vercel's @vercel/go runtime compiles everything into a single binary. This lets me:

  • Embed PHP-FPM and vendor.tar.gz into the function package
  • Use sync.Once for one-time cold start initialization
  • Keep PHP-FPM alive across warm invocations (container reuse)
  • Handle FastCGI protocol directly without extra dependencies

The Technical Deep Dive

Vendor Compression

The pack.sh script runs composer install --no-dev --optimize-autoloader, then compresses vendor/ into a tarball, excluding tests and docs:

# Run locally before deploy
bash scripts/pack.sh
Enter fullscreen mode Exit fullscreen mode

This typically gets a 200MB+ vendor directory down to ~30MB.

Build-Time Preparation

During Vercel's build phase, vercel-prepare.sh runs automatically:

  • Copies Laravel source files (app/, config/, routes/, etc.) into api/laravel/
  • Downloads the static PHP-FPM 8.4 binary from static-php-cli

These get bundled with the Go function — no network calls at runtime.

Cold Start Bootstrap

When the first request hits, the Go handler:

  1. Extracts the PHP-FPM binary to /tmp/php-fpm-bin
  2. Extracts vendor.tar.gz to /tmp/vendor
  3. Creates writable directories under /tmp/storage/
  4. Creates symlinks: /tmp/app → /var/task/laravel/app, /tmp/config → /var/task/laravel/config, etc.
  5. Generates a PHP entry point that overrides PackageManifest::$vendorPath and storagePath
  6. Starts PHP-FPM and waits for the socket

Subsequent requests on the same container skip all of this — PHP-FPM stays running.

Why the Symlinks?

This is the trickiest part. Composer's autoloader calculates $baseDir as two directories up from vendor/composer/. When vendor lives at /tmp/vendor/, it looks for app classes at /tmp/app/, /tmp/config/, etc. But our source files are at /var/task/laravel/. Symlinks bridge that gap.

Sessions with Redis

Cookie sessions are out (HTTP 494). File sessions are out (each invocation gets a fresh /tmp). The solution: Upstash Redis (free tier).

SESSION_DRIVER=redis
REDIS_CLIENT=predis
REDIS_URL=rediss://default:password@host:port
Enter fullscreen mode Exit fullscreen mode

Getting Started

It's a one-liner install. Run this in your Laravel project root:

curl -fsSL https://raw.githubusercontent.com/kristiansntsdev/vercel-laravel-go/main/install.sh | bash
Enter fullscreen mode Exit fullscreen mode

Then:

# 1. Compress vendor
bash scripts/pack.sh

# 2. Deploy
vercel deploy --prod
Enter fullscreen mode Exit fullscreen mode

Environment Variables

Set these in the Vercel dashboard:

Variable Value
APP_KEY php artisan key:generate --show
DB_* Your database credentials
REDIS_URL Upstash Redis URL

Everything else (cache paths, session config, log channel) is pre-configured in vercel.json.

CI/CD with GitHub Actions

The installer also sets up .github/workflows/deploy.yml. Add three secrets to your repo:

  • VERCEL_TOKEN — from vercel.com → Settings → Tokens
  • VERCEL_ORG_ID — your Vercel user/team ID
  • VERCEL_PROJECT_ID — from your project settings

Push to main and it deploys automatically.

Performance

Metric Value
Cold start ~2–3 seconds
Warm request Fast (PHP-FPM stays alive)
Bundle size ~50–80MB compressed
PHP version 8.4 (static binary)

Reducing Cold Starts

Use a free external pinger to keep your container warm:

Limitations

Be aware of the trade-offs:

  • No persistent filesystem — use a managed database
  • No queue workers — run those on Railway, Fly.io, or similar
  • No cron — use an external scheduler or Vercel Pro cron
  • 60s max request duration on Hobby plan
  • No static IP — use a serverless-friendly database (Neon, Supabase, PlanetScale)

Wrapping Up

If you've been stuck trying to deploy a heavy Laravel app on Vercel, give vercel-laravel-go a try. It works with Laravel 11 and 12, handles Filament apps without breaking a sweat, and deploys on Vercel's free tier.

The key insight: compress vendor, bundle PHP-FPM, and let Go handle the plumbing.


Star the repo if it helps: github.com/kristiansntsdev/vercel-laravel-go

Top comments (0)