Laravel 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 Laravel application, routes requests through public/index.php, and explains how to supply configuration and store durable state.
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 deployment-specific files:
routes/web.phpreturns a JSON response from Laravel and exposes/healthfor deployment smoke tests.Caddyfileexposes onlypublicand sends application routes topublic/index.php.Dockerfile.vercelinstalls production dependencies and copies the complete Laravel application into FrankenPHP.vercel.jsondeclares the container service and routes requests to it.
Copy link to headingSteps
Copy link to heading1. Create the Laravel application
Create a Laravel project:
Replace the default route in routes/web.php with a response that identifies the running framework:
Add a JSON health endpoint after the / route:
Laravel also registers /up as its default framework health route in bootstrap/app.php.
Copy link to heading2. Configure FrankenPHP
Create Caddyfile in the project root:
The site address reads the PORT environment variable and defaults to port 80. The document root prevents requests from accessing Laravel configuration or source files outside public.
Copy link to heading3. Add Dockerfile.vercel
Create the Dockerfile.vercel file in the project root:
The initial Composer installation skips scripts to preserve dependency-layer caching. After the application is copied, composer dump-autoload runs Laravel’s post-autoload-dump hooks, including package discovery.
The array cache and session drivers keep the example independent of a database. Configure shared external stores before using sessions, caches, or queues in a production application.
QUEUE_CONNECTION=sync executes jobs during the request and is suitable only for this minimal example. It does not provide asynchronous background processing.
Copy link to heading4. Exclude local files from the image
Create a .dockerignore file to specify excluded files:
The image must not contain .env files. Vercel injects the deployment’s environment variables when the container starts.
Copy link to heading5. Add vercel.json
Create a vercel.json file to configure your project:
The catch-all rewrite sends every public path to the Laravel service.
Copy link to heading6. Run locally
Laravel uses the APP_KEY environment variable to encrypt and authenticate cookies and session data.
Generate a local application key:
Store the displayed value in an uncommitted .env.local file:
The -L flag does not automatically inject .env.local into a container service. Export the file into the current shell, then start the application through Vercel's local container runtime:
Test both routes:
Copy link to heading7. Configure the application key
Add the generated key as an environment variable in your Vercel project:
Use a stable APP_KEY within each environment, so redeployments can decrypt existing cookies and encrypted data. Do not reuse the production key in Preview or Development unless those environments intentionally share encrypted data. Keep APP_DEBUG=false in production.
Copy link to heading8. Deploy to Vercel
Once ready, deploy your project to Vercel with a single command:
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 headingDatabase, sessions, and cache
Add database credentials as Vercel environment variables and install the matching PHP extension in Dockerfile.vercel, such as pdo_pgsql or pdo_mysql. Use an external database or Redis-compatible service for the state that must be shared across instances. Because Fluid compute can process concurrent requests in one instance and scale across multiple instances, use database connection pooling where available and size connection limits for aggregate concurrency.
Run database migrations as a separate release or continuous integration step:
Do not run migrations at container startup because multiple instances may start concurrently.
Copy link to headingFiles and logs
The container file system is not durable. Store uploads in an external object store such as Vercel Blob. LOG_CHANNEL=stderr sends application logs to Vercel runtime logs.
Copy link to headingDeployment optimization
Laravel’s php artisan optimize command caches configuration, events, routes, and views. However, configuration caching resolves environment variables when the command runs. Because Vercel provides values such as APP_KEY and database credentials at runtime, do not run the full command while building the container image.
Caches generated during the Docker build become immutable image contents, so every instance created from that deployment receives them. Files written while an instance is running may remain temporarily available to that instance, but they are not shared with other instances and disappear when the instance is recycled or the deployment changes.
You can cache components that do not depend on runtime environment variables:
You can also run php artisan route:cache. Laravel 13 can cache the closure routes in this example, although closures that capture unserializable values may still fail. Regenerate the route cache whenever routes change.
Avoid generating these caches from the container entrypoint. That would repeat the work whenever a new instance starts and add to its startup time. Keep the configuration uncached so each instance reads its Vercel runtime environment variables correctly.
Copy link to headingQueues and scheduled tasks
Long-running Laravel queue workers and the in-process scheduler assume the container stays active. Use Vercel Cron to call an authenticated HTTP endpoint on a schedule and Queues for durable multi-step work.
Copy link to headingTroubleshooting
Copy link to headingRequests return 502
Cause: FrankenPHP failed to start, listens only on localhost, or listens on a port other than the configured PORT.
Fix: Keep :{$PORT:80} in Caddyfile so the server follows Vercel's port contract.
Copy link to headingLaravel reports that no application encryption key is set
Cause: APP_KEY is missing from the deployment environment.
Fix: Generate the key once and add it with vercel env add APP_KEY. Redeploy after changing environment variables.
Copy link to headingRequests fail while writing cache or session files
Cause: storage or bootstrap/cache is not writable, or the application uses a local driver across multiple instances.
Fix: Preserve the ownership commands in Dockerfile.vercel and configure an external session or cache store when data must be shared.
Copy link to headingRelated resources
- Deploy Symfony with Docker
- Deploy PHP on Vercel with Docker
- Read the Vercel container images documentation
- Review the
vercel.jsonreference - Learn how Vercel Services routes traffic
- Learn how to run Docker on Vercel