---
title: Deploy PHP on Vercel with Docker
description: Build a PHP application with FrankenPHP and Docker, then deploy it to Vercel Functions with managed configuration, storage, and preview deployments.
url: "https://vercel.com/kb/guide/deploy-php-on-vercel-with-docker"
published: 2026-08-11
last_updated: 2026-10-29
authors: Anshuman Bhardwaj
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

You can deploy PHP applications to Vercel Functions with a Docker image that includes [FrankenPHP](https://frankenphp.dev/docs/docker/). FrankenPHP combines the PHP runtime with the Caddy web server to handle HTTP requests.

Vercel builds the image from your Dockerfile and stores it in [Vercel Container Registry](https://vercel.com/docs/container-registry). The deployed function scales with traffic and scales to zero when idle.

## Prerequisites

- A [Vercel account](https://vercel.com/signup).
  
- Docker Desktop or a running Docker daemon
  
- PHP 8.4 and [Composer 2](https://getcomposer.org/doc/00-intro.md) available in your terminal
  

For local development, you also need Node.js 24+ and the [Vercel CLI](https://vercel.com/docs/cli).

## How it works

The project has these parts:

- `public/index.php` returns JSON for `/` and `/health`.
  
- `Caddyfile` serves the public directory and sets the HTTP port.
  
- `composer.json` lists dependencies and `composer.lock` pins versions.
  
- `Dockerfile` packages the app, production dependencies, and FrankenPHP.
  

## Steps

### 1\. Initialize the project

Create the project directory and the document root:

```bash
mkdir vercel-docker-php
cd vercel-docker-php
mkdir public
```

Generate the Composer files required by the Docker build:

```bash
composer init --no-interaction --name=vercel/docker-php
composer update
```

The initial `composer.json` has no dependencies:

```json
{
  "name": "vercel/docker-php",
  "require": {}
}
```

Commit both Composer files. As you add packages with `composer require`, commit the updated lockfile so deployment builds install the versions you selected.

Create `public/index.php` to handle incoming requests:

```php
<?php

declare(strict_types=1);

header('Content-Type: application/json');
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

if ($path === '/') {
    echo json_encode(['message' => 'Hello from PHP on Vercel']);
    exit;
}

if ($path === '/health') {
    echo json_encode(['status' => 'ok']);
    exit;
}

http_response_code(404);
echo json_encode(['error' => 'not found']);
```

Create `Caddyfile` in the project root:

```plaintext
:{$PORT:80} {
    root * /app/public
    encode zstd gzip
    php_server
}
```

The site address reads `PORT` and falls back to port 80, matching Vercel's default. If you change the port, set `PORT` in your Vercel project settings so the server and Vercel use the same value.

Caddy binds to all interfaces by default and accepts any hostname at this address. It serves HTTP on port 80 while Vercel handles public HTTPS.

The project now contains the following files, with the Dockerfile from the next step still to come:

```plaintext
vercel-docker-php
├── Caddyfile
├── composer.json
├── composer.lock
└── public
    └── index.php
```

### 2\. Add `Dockerfile`

Create a `Dockerfile` in the project root:

```dockerfile
FROM dunglas/frankenphp:1-php8.4-alpine AS base
WORKDIR /app
RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"

FROM base AS dependencies
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
RUN apk add --no-cache unzip
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --no-progress --optimize-autoloader

FROM base AS runtime
COPY --from=dependencies /app/vendor ./vendor
COPY Caddyfile /etc/caddy/Caddyfile
COPY public ./public
ENV PORT=80
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]
```

Both stages use the same PHP runtime. Install required PHP extensions in the `base` stage to make them available during the build and at runtime.

Copying the Composer files before application code lets Docker reuse the dependency layer. Composer and the archive tool stay in the dependency stage, while the final image uses PHP's production settings.

Passing `--config` explicitly keeps the Dockerfile independent of where a given image version looks for its default `Caddyfile`.

For this PHP project, Vercel detects the root Dockerfile and selects the Container preset. No `vercel.json` is required when no other framework is detected.

If you use an existing project with another framework preset, link this directory to that project with `vercel link`, then select Container:

```bash
vercel project update --framework container --yes
```

Naming the file `Dockerfile.vercel` or `Containerfile.vercel` also makes the Container preset take precedence over another detected framework.

### 3\. Run locally

With Docker running, start the app from the project root. The `--local` option runs without linking a Vercel project:

```bash
vercel dev --local
```

Keep the server running and open another terminal. Use the URL printed by the CLI, replacing port 3000 below if needed:

```bash
curl -i http://localhost:3000/
curl -i http://localhost:3000/health
curl -i http://localhost:3000/missing
```

Check the status and JSON body for each request:

| Path       | Expected status | Expected JSON                            |
| ---------- | --------------- | ---------------------------------------- |
| `/`        | 200             | `{"message":"Hello from PHP on Vercel"}` |
| `/health`  | 200             | `{"status":"ok"}`                        |
| `/missing` | 404             | `{"error":"not found"}`                  |

The health route confirms that PHP can serve a request. It does not check a database or other external services.

### 4\. Deploy to Vercel

From the project root, sign in and deploy to production. Follow the CLI prompts to create a project or select the existing project you linked:

```bash
vercel login
vercel deploy --prod
```

Open the deployment URL and its `/health` path. They should return the same JSON responses as the local checks.

For Git deployments, commit the project files and import the repository in Vercel. Keep `Dockerfile` and `Caddyfile` at the project root.

## Extend your application

Load Composer's autoloader before using package classes. In `public/index.php`, add this line after `declare(strict_types=1);`:

```php
require __DIR__ . '/../vendor/autoload.php';
```

For [Laravel](https://vercel.com/kb/guide/laravel-php-with-docker) or [Symfony](https://vercel.com/kb/guide/symfony-php-with-docker), follow the framework's deployment steps. Include its application files and required PHP extensions in the image, configure its production build and caches, and keep the web root pointed at its public directory.

### Environment variables

Add variables under your project's **Settings > Environment Variables** or run `vercel env add NAME`. Select the environments that need each value, then redeploy for changes to take effect.

Refresh the linked project's Development settings, then start the local server:

```bash
vercel pull --environment=development
vercel dev
```

Use this linked mode when you need variables from Vercel. The `--local` mode used earlier skips project linking.

Read process variables with `getenv('NAME')` or your framework's configuration layer. Downloading a `.env` file alone does not load it into PHP. Keep secret files out of Git and the container image.

### Database and storage

Store persistent data outside the container. Local files can disappear when an instance is replaced, and other instances do not share them.

| Use case                      | Recommended service                                                                                                  |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Files and uploads             | [Vercel Blob](https://vercel.com/docs/vercel-blob)                                                                   |
| Relational data               | [Marketplace Postgres integration](https://vercel.com/marketplace/category/storage?category=storage&search=postgres) |
| Sessions and caching          | [Marketplace Redis integration](https://vercel.com/marketplace/category/storage?category=storage&search=redis)       |
| Frequently read configuration | [Vercel Global Config](https://vercel.com/docs/global-config)                                                        |

PHP sessions use files by default. Configure a shared session store so users can keep their session across instances.

Set database connection limits for your PHP concurrency and function instance count. Use your provider's supported pooling options.

### Iterate with preview deployments

After the first production deployment, pushes to non-production branches and pull requests create [preview deployments](https://vercel.com/docs/deployments/environments) by default. Run `vercel deploy` without `--prod` to create a preview from the CLI.

### Scale automatically with Fluid compute

Container images run as [Vercel Functions](https://vercel.com/docs/functions) on Fluid compute. Instances can handle concurrent requests and scale to zero after idle periods.

[Fluid compute pricing](https://vercel.com/docs/functions/usage-and-pricing) accounts for active CPU, provisioned memory, and invocations. Waiting for I/O does not consume active CPU time; memory and invocation usage are measured separately.

## Troubleshooting

### The deployment builds but requests return 502

Check the deployment's runtime logs for startup errors. Confirm that FrankenPHP listens on all interfaces and that its port matches Vercel's project setting.

The Caddyfile defaults to port 80. To use another port, set `PORT` in Vercel and redeploy. Docker's `EXPOSE` instruction does not set Vercel's routing port.

### The health route returns 404

Confirm that `public/index.php` contains the `/health` branch and that the final image includes the file. In Caddy, keep `root * /app/public` and `php_server` so requests can reach the front controller.

Other undefined paths intentionally return a JSON 404. If you receive `{"error":"not found"}`, the PHP entry point handled the request; check the requested path and application routes.

### Changes to PHP code are not reflected

Verify that you are opening the latest deployment and that its Dockerfile copies the changed files. Rebuild and redeploy when you change files included in the image.

If you add framework caches, regenerate them as part of that framework's deployment process. Generate configuration caches only when the intended environment values are available.

### Queue workers or scheduled commands stop

HTTP containers stop background workers when they scale down. Vercel sends `SIGTERM` and allows 30 seconds before forced termination.

For scheduled HTTP work, use [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs) and authenticate the endpoint. [Vercel Queues](https://vercel.com/docs/queues) can store messages, but your PHP application still needs a compatible producer and consumer integration.

## Run PHP workers with Vercel Daemons

PHP queue workers can send emails or generate reports while FrankenPHP handles web requests. [Vercel Daemons](https://vercel.com/docs/daemons) keep workers running from a container image with a PHP command configured in `vercel.json`.

Make each job safe to retry because Vercel restarts workers that exit or crash. Acknowledge queued jobs only after processing succeeds, and follow the [Daemons guide](https://vercel.com/kb/guide/vercel-daemons) to set instance counts and handle graceful shutdown.

## Frequently asked questions

### Can I deploy a Dockerfile on Vercel?

Yes. With the [Container preset](https://vercel.com/docs/functions/runtimes/container), Vercel builds a standard `Dockerfile` at your project root and runs the image as a Vercel Function. Configure your HTTP server to listen on port 80 or the project's `PORT` setting.

### Can I install PHP extensions and system packages?

Yes. Install required extensions and system packages in your Dockerfile. Put PHP extensions in the shared `base` stage so Composer and the application use them, and check compatibility with the image's PHP version.

### Will uploads and PHP sessions survive a container restart?

Files written inside a function container are not durable or shared between instances. Store uploads in object storage and configure PHP sessions to use a shared database or Redis store so they survive container restarts.

### Can I run PHP queue workers on Vercel?

Yes. Vercel Daemons keep PHP queue workers running independently of web traffic. You set the instance count, and the beta supports production deployments only.

## Next steps

- Follow the [Symfony deployment guide](https://vercel.com/kb/guide/symfony-php-with-docker) to package a Symfony app and configure it for production.
  
- Use the [Laravel deployment guide](https://vercel.com/kb/guide/laravel-php-with-docker) to configure your Docker image, environment variables, and persistent storage.
  
- Run your PHP backend and frontend in one project with [Vercel Services](https://vercel.com/docs/services).
  
- Use [Docker on Vercel](https://vercel.com/kb/guide/docker) to plan image storage and deployment workflows.
  
- Check the [Container runtime documentation](https://vercel.com/docs/functions/runtimes/container) for port settings, lifecycle behavior, and platform limits.

## Related Documentation

- [Vercel Functions](/docs/functions)
- [Vercel Container Registry](https://vercel.com/docs/container-registry)

## More Vercel Functions guides

- [Deploy Rust on Vercel with Docker](/kb/guide/deploy-rust-on-vercel-with-docker): Build a Rust application with Axum and Docker, then deploy it to Vercel Functions. Learn how to configure environment variables, managed storage, and preview deployments for your project.
- [Vercel vs Railway](/kb/guide/vercel-vs-railway): A detailed guide to Vercel vs Railway: serverless vs always-on containers, container images from a Dockerfile, framework-defined infrastructure, AI infrastructure, global CDN, security, and when to choose each platform for your project.
- [Vercel vs Render](/kb/guide/vercel-vs-render): A detailed guide to Vercel vs Render: compute models, AI infrastructure, Docker and container image support, background workers, and when to choose each platform for your project.