Symfony can run in a container-backed Vercel Function. FrankenPHP combines the PHP runtime with a production web server built on Caddy. Vercel builds the application into an image, stores it in Vercel Container Registry, and scales Function instances with traffic.
This guide deploys a stateless Symfony application, warms its production cache during the image build, and keeps runtime configuration outside the image.
Copy link to headingPrerequisites
- A Vercel account
- Docker Desktop or a running Docker daemon
- Vercel CLI (
npm install -g vercel) - Composer 2 when creating the project outside Docker
Copy link to headingHow it works
The project has these parts:
src/Controller/StatusController.phpreturns JSON for/and/health.Caddyfileserves static files only frompublic/and routes application requests topublic/index.php.Dockerfile.vercelinstalls production dependencies, clears the production cache, and copies the complete Symfony application into FrankenPHP.vercel.jsondeclares the container service and routes requests to it.
Copy link to headingSteps
Copy link to heading1. Create the Symfony application
Create a Symfony 7.4 LTS project:
Create src/Controller/StatusController.php:
The route attributes are discovered through the controller resource in config/routes.yaml.
Copy link to heading2. Configure FrankenPHP
Create Caddyfile in the project root:
The server follows Vercel's PORT, serves static files from public, and returns 404 for other PHP paths instead of executing them.
Copy link to heading3. Add Dockerfile.vercel
Create Dockerfile.vercel at the project root:
The dependency layer installs packages with Composer scripts disabled. After copying the source, the build regenerates an authoritative autoloader and explicitly clears and warms Symfony’s production cache.
Copy link to heading4. Exclude local files from the image
Create a .dockerignore file at the project root to exclude files from the Docker image:
Symfony's committed .env contains non-secret defaults. Keep production secrets in Vercel environment variables or uncommitted .env.local files, which this configuration excludes from the image.
Copy link to heading5. Add vercel.json
Create a vercel.json file at the project root to configure the Vercel project:
The catch-all rewrite sends every public path to the Symfony service.
Copy link to heading6. Run locally
Start the application through Vercel's local container runtime. The -L flag uses the local project configuration without linking to a Vercel project:
Test both routes:
Copy link to heading7. Deploy to Vercel
After both local requests succeed, deploy the project to production:
Run vercel deploy without --prod to create a Preview Deployment.
For Git deployments, import the repository in Vercel and keep Dockerfile.vercel, Caddyfile, and vercel.json at the configured project root.
Copy link to headingConfigure production services
Copy link to headingEnvironment variables
Add APP_SECRET, DATABASE_URL, and other application values in the Vercel project settings or with vercel env add NAME. Real environment variables override values from Symfony's configuration .env file.
When relying exclusively on runtime environment variables, you can generate an optimized empty environment map during the build with composer dump-env prod --empty. Do not compile production secret values into the image.
Copy link to headingDatabase and migrations
Install the PDO extension required by your database, such as pdo_pgsql or pdo_mysql, with install-php-extensions. A Vercel deployment can run multiple container instances, and each concurrent PHP request (or each FrankenPHP worker when worker mode is enabled) can open a connection. Use an external pooler or proxy when needed, and set its limits to match expected concurrency.
Use a serverless-compatible database, external connection pooler, or database proxy. Open connections lazily, keep pool sizes conservative, use short idle timeouts, and deploy the Function in a region close to the database. Run Doctrine migrations as a separate release or CI step rather than during container startup.
Copy link to headingCache, files, and logs
The image contains Symfony’s warmed production cache, which reduces startup work for new instances. Although /app/var is writable at runtime, its contents are ephemeral, local to one container instance, and discarded when that instance is replaced or scaled down. Do not use the local file system for uploads, sessions, queues, or other durable state. Store durable data in an external database, cache, or object store such as Vercel Blob. Write production logs to stderr so Vercel can collect them.
Copy link to headingAssets
If the project uses AssetMapper, run php bin/console asset-map:compile during the image build. If it uses Webpack Encore, build assets in a Node.js stage and copy the generated files into public/build.
Copy link to headingMessenger and scheduled commands
Long-running Symfony Messenger workers and in-process schedulers assume the container stays active. Do not run messenger:consume or an in-process scheduler as an always-on process because the Function may shut down midway. Use Vercel Cron for authenticated scheduled endpoints and Queues for durable multi-step work.
Copy link to headingOptionally enable FrankenPHP worker mode
Symfony 7.4 supports FrankenPHP worker mode natively. To enable it, add worker /app/public/index.php inside the php_server block. This boots one Symfony kernel per FrankenPHP worker and reuses it while the container instance remains active. Treat all in-memory state as ephemeral and local to that worker. Audit stateful services and implement ResetInterface where necessary. Symfony’s runtime recycles workers after 500 requests by default; configure this with FRANKENPHP_LOOP_MAX.
Copy link to headingTroubleshooting
Copy link to headingRequests return 502
Cause: FrankenPHP is listening on a fixed port or only on localhost.
Fix: Keep :{$PORT:80} in Caddyfile so the server follows Vercel's port contract.
Copy link to headingRoutes return 404
Cause: The document root or front-controller fallback is incorrect.
Fix: Keep root * /app/public and try_files {path} index.php, then confirm the route with php bin/console debug:router.
Copy link to headingThe application uses development configuration
Cause: APP_ENV or APP_DEBUG overrides the production defaults.
Fix: Set APP_ENV=prod and APP_DEBUG=0, clear the cache, and redeploy.
Copy link to headingRelated resources
- Deploy Laravel with Docker
- Deploy PHP on Vercel with Docker
- Learn how to run Docker on Vercel
- Read the Vercel container images documentation
- Review the
vercel.jsonreference - Learn how Vercel Services routes traffic