PHP applications need both a web server and a PHP runtime to receive HTTP traffic, and FrankenPHP combines the two in a single production server built on Caddy. Add a Dockerfile.vercel and set the service runtime to container. Vercel builds the image, stores it in the Vercel Container Registry, and serves it from a Function that autoscales with traffic and scales to zero when idle.
This guide deploys a minimal PHP JSON API, configures FrankenPHP to listen on Vercel's PORT, and explains how to use durable external storage instead of the container file system.
Copy link to headingPrerequisites
- A Vercel account
- Docker Desktop or a running Docker daemon
- Vercel CLI (
npm install -g vercel)
Copy link to headingHow it works
The project has these parts:
public/index.phpreturns JSON for/and/health.Caddyfileserves the public directory and binds FrankenPHP to$PORT.composer.lockpins PHP dependencies if the application adds any.Dockerfile.vercelinstalls production dependencies and copies the application into a FrankenPHP runtime.vercel.jsondeclares a container service and routes requests to it.
Copy link to headingSteps
Copy link to heading1. Initialize the project
Create the project directory and the document root:
Generate the Composer manifests. The image contains both composer.json and composer.lock, so create them now even if the application has no dependencies yet:
You should now have a minimal composer.json. Add production packages to require as the application grows, and commit composer.lock, so builds resolve the same versions:
Now, for the front controller, create a new file called public/index.php:
Create Caddyfile in the project root:
The site address reads PORT and falls back to 80, which matches Vercel's default container port. The leading colon listens on all container interfaces rather than only localhost, and because the address has no hostname, Caddy does not attempt to provision its own certificates for traffic that Vercel already terminates at the edge.
The project now contains the following files, with the two configuration files from the next steps still to come:
Copy link to heading2. Add Dockerfile.vercel
Create the Dockerfile.vercel file in the project root:
Copying composer.json and composer.lock before the application code lets Docker reuse the dependency layer. Composer stays in the first stage, so the final image contains only FrankenPHP, production packages, and application files.
The final stage runs as a non-root user. Passing --config explicitly keeps the Dockerfile independent of where a given image version looks for its default Caddyfile.
Copy link to heading3. Add vercel.json
Create a vercel.json file to configure your project:
The service selects the container runtime and points to the Dockerfile. The catch-all rewrite sends every public path to the FrankenPHP service.
Copy link to heading4. Run locally
Test the URL printed by the CLI:
Copy link to heading5. Deploy to Vercel
For Git deployments, import the repository in Vercel and keep Dockerfile.vercel, Caddyfile, and vercel.json at the configured project root.
Copy link to headingExtend your application
You have deployed a PHP service with a production web server. You can adapt this container model for Laravel, Symfony, or another framework. Copy the complete application into the final image, install the framework’s required PHP extensions, run its production build or cache commands, and keep the document root pointed at its public directory.
Copy link to headingEnvironment variables
Add variables in Vercel Dashboard > Project Settings or with vercel env add NAME. Pull development values with vercel env pull, then read them with getenv() or your framework's configuration layer. Do not commit .env files or copy them into the image.
Copy link to headingDatabase and storage
The container file system is not persistent because it runs on Vercel Functions, which scale down when idle.
| Use case | Recommended service |
|---|---|
| Files and uploads | Vercel Blob |
| Relational data | Marketplace Postgres integration |
| Sessions and caching | Marketplace Redis integration |
| Frequently read configuration | Vercel Global Config |
Store PHP sessions outside the file system. Keep persistent database connections and worker counts conservative, as each Vercel Function instance creates additional connections.
Copy link to headingIterate with preview deployments
A connected repository receives a unique Preview Deployment for each branch push or pull request. Run vercel deploy without --prod for a disposable CLI preview.
Copy link to headingScale automatically with Fluid compute
Vercel Services run on Fluid compute by default. Instances handle concurrent traffic and scale to zero when idle. Active CPU billing pauses during I/O waits and when no requests run; memory and invocation charges still apply.
Copy link to headingTroubleshooting
Copy link to headingThe deployment builds but requests return 502
Cause: Caddy is listening on a hardcoded port or only on localhost.
Fix: Keep :{$PORT:80} in the Caddyfile. A Docker EXPOSE instruction does not configure the port Vercel routes to.
Copy link to headingRoutes other than / return 404
Cause: The document root or front-controller routing does not send the request to public/index.php.
Fix: Keep root * /app/public and php_server. For a framework, copy its actual public directory and verify rewrite behavior using the final image.
Copy link to headingChanges to PHP code are not reflected
Cause: OPcache or a framework cache contains stale production artifacts, or the updated source was not copied into the final image.
Fix: Ensure the Dockerfile copies the changed directory after dependency installation. Build framework caches during the image build and regenerate them on every deployment.
Copy link to headingQueue workers or scheduled commands stop
Cause: Long-running PHP workers and in-process schedulers assume the container remains alive.
Fix: Use Vercel Cron to call HTTP endpoints and Queues for durable work. Let FrankenPHP handle SIGTERM and finish active requests.
Copy link to headingNext steps
- Deploy Symfony with Docker
- Deploy Laravel with Docker
- Learn how Vercel Services routes traffic
- Learn how to run Docker on Vercel
- Read the container images documentation